Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 90f3d1f

Browse files
committed
fix: add mixins for eloquent builder and relation but retain this return type
1 parent 775ed61 commit 90f3d1f

File tree

12 files changed

+461
-36
lines changed

12 files changed

+461
-36
lines changed

extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ services:
1818
arguments:
1919
morphMap: %laravel.morphMap%
2020
tags: [phpstan.methodParameterClosureTypeExtension]
21+
- class: Recoded\PHPStanLaravel\Extensions\Eloquent\BuilderMixinExtension
22+
tags: [phpstan.broker.methodsClassReflectionExtension]
23+
- class: Recoded\PHPStanLaravel\Extensions\Eloquent\RelationMixinExtension
24+
tags: [phpstan.broker.methodsClassReflectionExtension]
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Recoded\PHPStanLaravel\Extensions\Eloquent;
6+
7+
use PHPStan\Analyser\OutOfClassScope;
8+
use PHPStan\Reflection\ClassReflection;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Reflection\MethodsClassReflectionExtension;
11+
use PHPStan\Type\ObjectType;
12+
13+
final class BuilderMixinExtension implements MethodsClassReflectionExtension
14+
{
15+
/** @var array<string, array<int, string>> */
16+
private array $mixinMethods = [];
17+
private ObjectType $queryBuilder;
18+
19+
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
20+
{
21+
return $classReflection->is('Illuminate\Database\Eloquent\Builder')
22+
&& $classReflection->hasNativeMethod($methodName) === false
23+
&& $this->queryBuilder()->hasMethod($methodName)->yes();
24+
}
25+
26+
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
27+
{
28+
$scope = new OutOfClassScope();
29+
$methodReflection = $this->queryBuilder()->getMethod($methodName, $scope);
30+
$lowercaseMethodName = strtolower($methodName);
31+
32+
if (in_array($lowercaseMethodName, $this->mixinMethods($classReflection), true)) {
33+
return $methodReflection;
34+
}
35+
36+
return new BuilderMixinMethodReflection($classReflection, $methodReflection);
37+
}
38+
39+
/**
40+
* @return array<int, string>
41+
*/
42+
private function mixinMethods(ClassReflection $classReflection): array
43+
{
44+
$cacheKey = $classReflection->getCacheKey();
45+
46+
if (!array_key_exists($cacheKey, $this->mixinMethods)) {
47+
/** @var array<int, string> $passthru */
48+
$passthru = $classReflection->getNativeReflection()->getDefaultProperties()['passthru'];
49+
$this->mixinMethods[$cacheKey] = array_map('strtolower', $passthru);
50+
}
51+
52+
return $this->mixinMethods[$cacheKey];
53+
}
54+
55+
private function queryBuilder(): ObjectType
56+
{
57+
return $this->queryBuilder ??= new ObjectType('Illuminate\Database\Query\Builder');
58+
}
59+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Recoded\PHPStanLaravel\Extensions\Eloquent;
6+
7+
use PHPStan\Reflection\ClassMemberReflection;
8+
use PHPStan\Reflection\ClassReflection;
9+
use PHPStan\Reflection\FunctionVariant;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Reflection\ParametersAcceptor;
12+
use PHPStan\TrinaryLogic;
13+
use PHPStan\Type\ThisType;
14+
use PHPStan\Type\Type;
15+
16+
final class BuilderMixinMethodReflection implements MethodReflection
17+
{
18+
/** @var \PHPStan\Reflection\ParametersAcceptor[] */
19+
private array $variants;
20+
21+
public function __construct(
22+
private ClassReflection $classReflection,
23+
private MethodReflection $methodReflection,
24+
) {
25+
}
26+
27+
public function getDeclaringClass(): ClassReflection
28+
{
29+
return $this->classReflection;
30+
}
31+
32+
public function isStatic(): bool
33+
{
34+
return false;
35+
}
36+
37+
public function isPrivate(): bool
38+
{
39+
return $this->methodReflection->isPrivate();
40+
}
41+
42+
public function isPublic(): bool
43+
{
44+
return $this->methodReflection->isPublic();
45+
}
46+
47+
public function getDocComment(): ?string
48+
{
49+
return $this->methodReflection->getDocComment();
50+
}
51+
52+
public function getName(): string
53+
{
54+
return $this->methodReflection->getName();
55+
}
56+
57+
public function getPrototype(): ClassMemberReflection
58+
{
59+
return $this->methodReflection->getPrototype();
60+
}
61+
62+
public function getVariants(): array
63+
{
64+
return $this->variants ??= array_map(
65+
fn (ParametersAcceptor $variant) => new FunctionVariant(
66+
$variant->getTemplateTypeMap(),
67+
$variant->getResolvedTemplateTypeMap(),
68+
$variant->getParameters(),
69+
$variant->isVariadic(),
70+
new ThisType($this->classReflection),
71+
),
72+
$this->methodReflection->getVariants(),
73+
);
74+
}
75+
76+
public function isDeprecated(): TrinaryLogic
77+
{
78+
return $this->methodReflection->isDeprecated();
79+
}
80+
81+
public function getDeprecatedDescription(): ?string
82+
{
83+
return $this->methodReflection->getDeprecatedDescription();
84+
}
85+
86+
public function isFinal(): TrinaryLogic
87+
{
88+
return $this->methodReflection->isFinal();
89+
}
90+
91+
public function isInternal(): TrinaryLogic
92+
{
93+
return $this->methodReflection->isInternal();
94+
}
95+
96+
public function getThrowType(): ?Type
97+
{
98+
return $this->methodReflection->getThrowType();
99+
}
100+
101+
public function hasSideEffects(): TrinaryLogic
102+
{
103+
return $this->methodReflection->hasSideEffects();
104+
}
105+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Recoded\PHPStanLaravel\Extensions\Eloquent;
6+
7+
use Illuminate\Database\Eloquent\Model;
8+
use PHPStan\Analyser\OutOfClassScope;
9+
use PHPStan\Reflection\ClassReflection;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Reflection\MethodsClassReflectionExtension;
12+
use PHPStan\Reflection\ParametersAcceptorSelector;
13+
use PHPStan\Type\ObjectType;
14+
15+
final class RelationMixinExtension implements MethodsClassReflectionExtension
16+
{
17+
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
18+
{
19+
$missing = $classReflection->is('Illuminate\Database\Eloquent\Relations\Relation')
20+
&& $classReflection->hasNativeMethod($methodName) === false;
21+
22+
if (!$missing) {
23+
return false;
24+
}
25+
26+
$related = $classReflection->getActiveTemplateTypeMap()->getType('TRelated');
27+
28+
if ($related === null) {
29+
return false;
30+
}
31+
32+
if (!(new ObjectType('Illuminate\Database\Eloquent\Model'))->isSuperTypeOf($related)->yes()) {
33+
return false;
34+
}
35+
36+
$builderMethod = $related->getMethod('newEloquentBuilder', new OutOfClassScope());
37+
38+
$returnType = ParametersAcceptorSelector::selectSingle($builderMethod->getVariants())->getReturnType();
39+
40+
if (!$returnType->isObject()->yes()) {
41+
return false;
42+
}
43+
44+
return $returnType->hasMethod($methodName)->yes();
45+
}
46+
47+
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
48+
{
49+
/** @var \PHPStan\Type\ObjectType $related */
50+
$related = $classReflection->getActiveTemplateTypeMap()->getType('TRelated');
51+
52+
$builderMethod = $related->getMethod('newEloquentBuilder', new OutOfClassScope());
53+
54+
/** @var \PHPStan\Type\ObjectType $returnType */
55+
$returnType = ParametersAcceptorSelector::selectSingle($builderMethod->getVariants())->getReturnType();
56+
57+
$builderMethod = $returnType->getMethod($methodName, new OutOfClassScope());
58+
59+
return new RelationMixinMethodReflection($classReflection, $builderMethod);
60+
}
61+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Recoded\PHPStanLaravel\Extensions\Eloquent;
6+
7+
use PHPStan\Reflection\ClassMemberReflection;
8+
use PHPStan\Reflection\ClassReflection;
9+
use PHPStan\Reflection\FunctionVariant;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Reflection\ParametersAcceptor;
12+
use PHPStan\TrinaryLogic;
13+
use PHPStan\Type\NeverType;
14+
use PHPStan\Type\ObjectType;
15+
use PHPStan\Type\ThisType;
16+
use PHPStan\Type\Type;
17+
18+
final class RelationMixinMethodReflection implements MethodReflection
19+
{
20+
/** @var \PHPStan\Reflection\ParametersAcceptor[] */
21+
private array $variants;
22+
23+
public function __construct(
24+
private ClassReflection $classReflection,
25+
private MethodReflection $methodReflection,
26+
) {
27+
}
28+
29+
public function getDeclaringClass(): ClassReflection
30+
{
31+
return $this->methodReflection->getDeclaringClass();
32+
}
33+
34+
public function isStatic(): bool
35+
{
36+
return false;
37+
}
38+
39+
public function isPrivate(): bool
40+
{
41+
return $this->methodReflection->isPrivate();
42+
}
43+
44+
public function isPublic(): bool
45+
{
46+
return $this->methodReflection->isPublic();
47+
}
48+
49+
public function getDocComment(): ?string
50+
{
51+
return $this->methodReflection->getDocComment();
52+
}
53+
54+
public function getName(): string
55+
{
56+
return $this->methodReflection->getName();
57+
}
58+
59+
public function getPrototype(): ClassMemberReflection
60+
{
61+
return $this->methodReflection->getPrototype();
62+
}
63+
64+
public function getVariants(): array
65+
{
66+
if (isset($this->variants)) {
67+
return $this->variants;
68+
}
69+
70+
$builderType = new ObjectType('Illuminate\Database\Eloquent\Builder');
71+
$thisType = new ThisType($this->classReflection);
72+
73+
return $this->variants = array_map(
74+
static function (ParametersAcceptor $variant) use ($builderType, $thisType) {
75+
$returnsBuilder = $variant->getReturnType() instanceof NeverType === false
76+
&& $builderType->isSuperTypeOf($variant->getReturnType())->yes();
77+
78+
return new FunctionVariant(
79+
$variant->getTemplateTypeMap(),
80+
$variant->getResolvedTemplateTypeMap(),
81+
$variant->getParameters(),
82+
$variant->isVariadic(),
83+
$returnsBuilder ? $thisType : $variant->getReturnType(),
84+
);
85+
},
86+
$this->methodReflection->getVariants(),
87+
);
88+
}
89+
90+
public function isDeprecated(): TrinaryLogic
91+
{
92+
return $this->methodReflection->isDeprecated();
93+
}
94+
95+
public function getDeprecatedDescription(): ?string
96+
{
97+
return $this->methodReflection->getDeprecatedDescription();
98+
}
99+
100+
public function isFinal(): TrinaryLogic
101+
{
102+
return $this->methodReflection->isFinal();
103+
}
104+
105+
public function isInternal(): TrinaryLogic
106+
{
107+
return $this->methodReflection->isInternal();
108+
}
109+
110+
public function getThrowType(): ?Type
111+
{
112+
return $this->methodReflection->getThrowType();
113+
}
114+
115+
public function hasSideEffects(): TrinaryLogic
116+
{
117+
return $this->methodReflection->hasSideEffects();
118+
}
119+
}

src/Extensions/Eloquent/WhereHasBuilderType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public function isMethodSupported(MethodReflection $method, ParameterReflection
4343
}
4444

4545
return $parameter->getName() === 'callback'
46-
&& $method->getDeclaringClass()->getName() === 'Illuminate\Database\Eloquent\Builder';
46+
&& $method->getDeclaringClass()->is('Illuminate\Database\Eloquent\Builder');
4747
}
4848

4949
public function getTypeFromMethodCall(MethodReflection $method, MethodCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type

src/Extensions/Eloquent/WhereHasMorphBuilderType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function isMethodSupported(MethodReflection $method, ParameterReflection
5050
}
5151

5252
return $parameter->getName() === 'callback'
53-
&& $method->getDeclaringClass()->getName() === 'Illuminate\Database\Eloquent\Builder';
53+
&& $method->getDeclaringClass()->is('Illuminate\Database\Eloquent\Builder');
5454
}
5555

5656
public function getTypeFromMethodCall(MethodReflection $method, MethodCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type

stubs/database/eloquent/relations/belongs-to-many.stub

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Illuminate\Database\Eloquent\Relations;
44

5+
use Closure;
56
use Illuminate\Database\Eloquent\Model;
67

78
/**
@@ -108,7 +109,7 @@ class BelongsToMany extends Relation
108109

109110
/**
110111
* @template TOther
111-
* @param \Closure(): TOther|array $columns
112+
* @param \Closure(): TOther|string[] $columns
112113
* @param \Closure(): TOther|null $callback
113114
* @return TRelated|TOther
114115
*/

0 commit comments

Comments
 (0)