Skip to content

Commit 6c5e882

Browse files
authored
fix: consider union template types on builders (#1701)
1 parent 5863603 commit 6c5e882

3 files changed

Lines changed: 81 additions & 23 deletions

File tree

phpstan-baseline.neon

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,6 @@ parameters:
3030
count: 1
3131
path: src/Properties/ModelAccessorExtension.php
3232

33-
-
34-
message: "#^PHPDoc tag @var assumes the expression with type PHPStan\\\\Type\\\\Type\\|null is always PHPStan\\\\Type\\\\ObjectType but it's error\\-prone and dangerous\\.$#"
35-
count: 1
36-
path: src/ReturnTypes/BuilderModelFindExtension.php
37-
3833
-
3934
message: "#^Constant LARAVEL_VERSION not found\\.$#"
4035
count: 1

src/ReturnTypes/BuilderModelFindExtension.php

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Illuminate\Database\Eloquent\Builder;
88
use Illuminate\Database\Eloquent\Collection;
9+
use Illuminate\Database\Eloquent\Model;
910
use Illuminate\Database\Query\Builder as QueryBuilder;
1011
use Illuminate\Support\Str;
1112
use NunoMaduro\Larastan\Methods\ModelTypeHelper;
@@ -78,31 +79,45 @@ public function getTypeFromMethodCall(
7879
return null;
7980
}
8081

81-
/** @var ObjectType $model */
82-
$model = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TModelClass');
82+
/** @var Type $modelClassType */
83+
$modelClassType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TModelClass');
84+
85+
if ((new ObjectType(Model::class))->isSuperTypeOf($modelClassType)->no()) {
86+
return null;
87+
}
88+
8389
$returnType = $methodReflection->getVariants()[0]->getReturnType();
8490
$argType = $scope->getType($methodCall->getArgs()[0]->value);
8591

86-
$returnType = ModelTypeHelper::replaceStaticTypeWithModel($returnType, $model->getClassName());
92+
if ($argType instanceof MixedType) {
93+
return $returnType;
94+
}
8795

88-
if ($argType->isIterable()->yes()) {
89-
if (in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
90-
return $this->collectionHelper->determineCollectionClass($model->getClassName());
91-
}
96+
$models = [];
9297

93-
return TypeCombinator::remove($returnType, $model);
94-
}
98+
foreach ($modelClassType->getObjectClassReflections() as $objectClassReflection) {
99+
$modelName = $objectClassReflection->getName();
95100

96-
if ($argType instanceof MixedType) {
97-
return $returnType;
101+
$returnType = ModelTypeHelper::replaceStaticTypeWithModel($returnType, $modelName);
102+
103+
if ($argType->isIterable()->yes()) {
104+
if (in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
105+
$models[] = $this->collectionHelper->determineCollectionClass($modelName);
106+
continue;
107+
}
108+
109+
$models[] = TypeCombinator::remove($returnType, new ObjectType($modelName));
110+
} else {
111+
$models[] = TypeCombinator::remove(
112+
TypeCombinator::remove(
113+
$returnType,
114+
new ArrayType(new MixedType(), $modelClassType)
115+
),
116+
new ObjectType(Collection::class)
117+
);
118+
}
98119
}
99120

100-
return TypeCombinator::remove(
101-
TypeCombinator::remove(
102-
$returnType,
103-
new ArrayType(new MixedType(), $model)
104-
),
105-
new ObjectType(Collection::class)
106-
);
121+
return count($models) > 0 ? TypeCombinator::union(...$models) : null;
107122
}
108123
}

tests/Type/data/eloquent-builder.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use App\Post;
88
use App\PostBuilder;
9+
use App\Team;
910
use App\User;
1011
use Illuminate\Database\Eloquent\Builder;
1112
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
@@ -451,6 +452,13 @@ function testChunkOnEloquentBuilder()
451452
User::chunk(1000, fn ($collection) => assertType('Illuminate\Database\Eloquent\Collection<int, App\User>', $collection));
452453
}
453454

455+
/** @param Builder<User|Team> $builder */
456+
function testUnionBuilder(Builder $builder)
457+
{
458+
assertType('App\Team|App\User', $builder->findOrFail(4));
459+
assertType('Illuminate\Database\Eloquent\Builder<App\Team|App\User>', $builder->where('id', 5));
460+
}
461+
454462
class Foo extends Model
455463
{
456464
/** @phpstan-use FooTrait<Foo> */
@@ -492,3 +500,43 @@ public function testQuery(): Builder
492500
class CustomBuilder extends Builder
493501
{
494502
}
503+
504+
/**
505+
* @template TModel of User|Team
506+
*/
507+
abstract class UnionClass
508+
{
509+
/**
510+
* @return TModel
511+
*/
512+
public function test(int $id): Model
513+
{
514+
assertType('TModel of App\Team|App\User (class EloquentBuilder\UnionClass, argument)', $this->getQuery()->findOrFail($id));
515+
516+
return $this->getQuery()->findOrFail($id);
517+
}
518+
519+
/**
520+
* @return Builder<TModel>
521+
*/
522+
abstract public function getQuery(): Builder;
523+
}
524+
525+
/**
526+
* @extends UnionClass<Team>
527+
*/
528+
class TeamClass extends UnionClass
529+
{
530+
public function foo()
531+
{
532+
assertType('App\Team', $this->test(5));
533+
}
534+
535+
/**
536+
* {@inheritdoc}
537+
*/
538+
public function getQuery(): Builder
539+
{
540+
return Team::query();
541+
}
542+
}

0 commit comments

Comments
 (0)