Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 79de0a6

Browse files
committed
fix: prevent return type errors when type has been overridden
1 parent 31ad3e9 commit 79de0a6

File tree

11 files changed

+471
-2
lines changed

11 files changed

+471
-2
lines changed

‎.github/workflows/e2e-tests.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,10 @@ jobs:
264264
cd e2e/bug-11857
265265
composer install
266266
../../bin/phpstan
267+
- script: |
268+
cd e2e/bug-12585
269+
composer install
270+
../../bin/phpstan
267271
- script: |
268272
cd e2e/result-cache-meta-extension
269273
composer install

‎e2e/bug-12585/.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/vendor
2+
composer.lock

‎e2e/bug-12585/composer.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"autoload-dev": {
3+
"classmap": ["src/"]
4+
}
5+
}

‎e2e/bug-12585/phpstan.neon‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
parameters:
2+
level: 8
3+
paths:
4+
- src
5+
6+
services:
7+
-
8+
class: Bug12585\EloquentBuilderRelationParameterExtension
9+
tags:
10+
- phpstan.dynamicMethodParameterTypeExtension

‎e2e/bug-12585/src/extension.php‎

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
<?php
2+
3+
namespace Bug12585;
4+
5+
use PHPStan\Analyser\Scope;
6+
use PHPStan\Reflection\MethodReflection;
7+
use PHPStan\Reflection\ParameterReflection;
8+
use PHPStan\Reflection\PassedByReference;
9+
use PHPStan\Reflection\ReflectionProvider;
10+
use PHPStan\Type\ClosureType;
11+
use PHPStan\Type\DynamicMethodParameterTypeExtension;
12+
use PHPStan\Type\Generic\GenericObjectType;
13+
use PHPStan\Type\MixedType;
14+
use PHPStan\Type\ObjectType;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\TypeWithClassName;
17+
use PHPStan\Type\TypeCombinator;
18+
use PHPStan\Type\VerbosityLevel;
19+
use PhpParser\Node\Expr\MethodCall;
20+
use PhpParser\Node\VariadicPlaceholder;
21+
22+
use function array_push;
23+
use function array_shift;
24+
use function count;
25+
use function explode;
26+
use function in_array;
27+
28+
final class EloquentBuilderRelationParameterExtension implements DynamicMethodParameterTypeExtension
29+
{
30+
/** @var list<string> */
31+
private array $methods = ['whereHas', 'withWhereHas'];
32+
33+
public function __construct(private ReflectionProvider $reflectionProvider)
34+
{
35+
}
36+
37+
public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
38+
{
39+
if (! $methodReflection->getDeclaringClass()->is(Builder::class)) {
40+
return false;
41+
}
42+
43+
if (! in_array($methodReflection->getName(), $this->methods, strict: true)) {
44+
return false;
45+
}
46+
47+
return $parameter->getName() === 'callback';
48+
}
49+
50+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, ParameterReflection $parameter, Scope $scope): Type|null
51+
{
52+
$method = $methodReflection->getName();
53+
$relations = $this->getRelationsFromMethodCall($methodCall, $scope);
54+
$models = $this->getModelsFromRelations($relations);
55+
56+
if (count($models) === 0) {
57+
return null;
58+
}
59+
60+
$type = $this->getBuilderTypeForModels($models);
61+
62+
if ($method === 'withWhereHas') {
63+
$type = TypeCombinator::union($type, ...$relations);
64+
}
65+
66+
return new ClosureType([new ClosureQueryParameter('query', $type)], new MixedType(), false);
67+
}
68+
69+
/**
70+
* @param array<int, Type> $relations
71+
* @return array<int, string>
72+
*/
73+
private function getModelsFromRelations(array $relations): array
74+
{
75+
$models = [];
76+
77+
foreach ($relations as $relation) {
78+
$classNames = $relation->getTemplateType(Relation::class, 'TRelatedModel')->getObjectClassNames();
79+
foreach ($classNames as $className) {
80+
$models[] = $className;
81+
}
82+
}
83+
84+
return $models;
85+
}
86+
87+
/** @return array<int, Type> */
88+
private function getRelationsFromMethodCall(MethodCall $methodCall, Scope $scope): array
89+
{
90+
$relationType = null;
91+
92+
foreach ($methodCall->args as $arg) {
93+
if ($arg instanceof VariadicPlaceholder) {
94+
continue;
95+
}
96+
97+
if ($arg->name === null || $arg->name->toString() === 'relation') {
98+
$relationType = $scope->getType($arg->value);
99+
break;
100+
}
101+
}
102+
103+
if ($relationType === null) {
104+
return [];
105+
}
106+
107+
$calledOnModels = $scope->getType($methodCall->var)
108+
->getTemplateType(Builder::class, 'TModel')
109+
->getObjectClassNames();
110+
111+
$values = array_map(fn ($type) => $type->getValue(), $relationType->getConstantStrings());
112+
$relationTypes = [$relationType];
113+
114+
foreach ($values as $relation) {
115+
$relationTypes = array_merge(
116+
$relationTypes,
117+
$this->getRelationTypeFromString($calledOnModels, explode('.', $relation), $scope)
118+
);
119+
}
120+
121+
return array_values(array_filter(
122+
$relationTypes,
123+
static fn ($r) => (new ObjectType(Relation::class))->isSuperTypeOf($r)->yes()
124+
));
125+
}
126+
127+
/**
128+
* @param list<string> $calledOnModels
129+
* @param list<string> $relationParts
130+
* @return list<Type>
131+
*/
132+
private function getRelationTypeFromString(array $calledOnModels, array $relationParts, Scope $scope): array
133+
{
134+
$relations = [];
135+
136+
while ($relationName = array_shift($relationParts)) {
137+
$relations = [];
138+
$relatedModels = [];
139+
140+
foreach ($calledOnModels as $model) {
141+
$modelType = new ObjectType($model);
142+
143+
if (! $modelType->hasMethod($relationName)->yes()) {
144+
continue;
145+
}
146+
147+
$relationType = $modelType->getMethod($relationName, $scope)->getVariants()[0]->getReturnType();
148+
149+
if (! (new ObjectType(Relation::class))->isSuperTypeOf($relationType)->yes()) {
150+
continue;
151+
}
152+
153+
$relations[] = $relationType;
154+
155+
array_push($relatedModels, ...$relationType->getTemplateType(Relation::class, 'TRelatedModel')->getObjectClassNames());
156+
}
157+
158+
$calledOnModels = $relatedModels;
159+
}
160+
161+
return $relations;
162+
}
163+
164+
private function determineBuilderName(string $modelClassName): string
165+
{
166+
$method = $this->reflectionProvider->getClass($modelClassName)->getNativeMethod('query');
167+
168+
$returnType = $method->getVariants()[0]->getReturnType();
169+
170+
if (in_array(Builder::class, $returnType->getReferencedClasses(), true)) {
171+
return Builder::class;
172+
}
173+
174+
$classNames = $returnType->getObjectClassNames();
175+
176+
if (count($classNames) === 1) {
177+
return $classNames[0];
178+
}
179+
180+
return $returnType->describe(VerbosityLevel::value());
181+
}
182+
183+
/**
184+
* @param array<int, string|TypeWithClassName>|string|TypeWithClassName $models
185+
* @return ($models is array<int, string|TypeWithClassName> ? Type : ObjectType)
186+
*/
187+
private function getBuilderTypeForModels(array|string|TypeWithClassName $models): Type
188+
{
189+
$models = is_array($models) ? $models : [$models];
190+
$models = array_unique($models, SORT_REGULAR);
191+
192+
$mappedModels = [];
193+
foreach ($models as $model) {
194+
if (is_string($model)) {
195+
$mappedModels[$model] = new ObjectType($model);
196+
} else {
197+
$mappedModels[$model->getClassName()] = $model;
198+
}
199+
}
200+
201+
$groupedByBuilder = [];
202+
foreach ($mappedModels as $class => $type) {
203+
$builderName = $this->determineBuilderName($class);
204+
$groupedByBuilder[$builderName][] = $type;
205+
}
206+
207+
$builderTypes = [];
208+
foreach ($groupedByBuilder as $builder => $models) {
209+
$builderReflection = $this->reflectionProvider->getClass($builder);
210+
211+
$builderTypes[] = $builderReflection->isGeneric()
212+
? new GenericObjectType($builder, [TypeCombinator::union(...$models)])
213+
: new ObjectType($builder);
214+
}
215+
216+
return TypeCombinator::union(...$builderTypes);
217+
}
218+
}
219+
220+
final class ClosureQueryParameter implements ParameterReflection
221+
{
222+
public function __construct(private string $name, private Type $type)
223+
{
224+
}
225+
226+
public function getName(): string
227+
{
228+
return $this->name;
229+
}
230+
231+
public function isOptional(): bool
232+
{
233+
return false;
234+
}
235+
236+
public function getType(): Type
237+
{
238+
return $this->type;
239+
}
240+
241+
public function passedByReference(): PassedByReference
242+
{
243+
return PassedByReference::createNo();
244+
}
245+
246+
public function isVariadic(): bool
247+
{
248+
return false;
249+
}
250+
251+
public function getDefaultValue(): Type|null
252+
{
253+
return null;
254+
}
255+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /