Skip to content

Commit ad171d4

Browse files
committed
fix: model relation properties
1 parent 8d433ed commit ad171d4

File tree

3 files changed

+13
-97
lines changed

3 files changed

+13
-97
lines changed

src/Properties/ModelRelationsExtension.php

+10-92
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,20 @@
55
namespace Larastan\Larastan\Properties;
66

77
use Illuminate\Database\Eloquent\Model;
8-
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
9-
use Illuminate\Database\Eloquent\Relations\HasMany;
10-
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
11-
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
12-
use Illuminate\Database\Eloquent\Relations\MorphMany;
13-
use Illuminate\Database\Eloquent\Relations\MorphTo;
14-
use Illuminate\Database\Eloquent\Relations\MorphToMany;
158
use Illuminate\Database\Eloquent\Relations\Relation;
169
use Illuminate\Support\Str;
1710
use Larastan\Larastan\Concerns;
1811
use Larastan\Larastan\Reflection\ReflectionHelper;
1912
use Larastan\Larastan\Support\CollectionHelper;
2013
use PHPStan\Analyser\OutOfClassScope;
2114
use PHPStan\Reflection\ClassReflection;
22-
use PHPStan\Reflection\MissingMethodFromReflectionException;
2315
use PHPStan\Reflection\PropertiesClassReflectionExtension;
2416
use PHPStan\Reflection\PropertyReflection;
25-
use PHPStan\ShouldNotHappenException;
26-
use PHPStan\Type\ErrorType;
27-
use PHPStan\Type\Generic\GenericObjectType;
2817
use PHPStan\Type\IntegerRangeType;
2918
use PHPStan\Type\IntersectionType;
30-
use PHPStan\Type\MixedType;
3119
use PHPStan\Type\NeverType;
32-
use PHPStan\Type\NullType;
3320
use PHPStan\Type\ObjectType;
3421
use PHPStan\Type\Type;
35-
use PHPStan\Type\TypeCombinator;
3622
use PHPStan\Type\TypeTraverser;
3723
use PHPStan\Type\UnionType;
3824

@@ -66,18 +52,12 @@ public function hasProperty(ClassReflection $classReflection, string $propertyNa
6652
}
6753

6854
foreach ($methodNames as $methodName) {
69-
$hasNativeMethod = $classReflection->hasNativeMethod($methodName);
70-
71-
if (! $hasNativeMethod) {
55+
if (! $classReflection->hasNativeMethod($methodName)) {
7256
continue;
7357
}
7458

7559
$returnType = $classReflection->getNativeMethod($methodName)->getVariants()[0]->getReturnType();
7660

77-
if ($returnType->getTemplateType(Relation::class, 'TRelatedModel') instanceof ErrorType) {
78-
continue;
79-
}
80-
8161
if ((new ObjectType(Relation::class))->isSuperTypeOf($returnType)->yes()) {
8262
return true;
8363
}
@@ -86,92 +66,30 @@ public function hasProperty(ClassReflection $classReflection, string $propertyNa
8666
return false;
8767
}
8868

89-
/**
90-
* @throws ShouldNotHappenException
91-
* @throws MissingMethodFromReflectionException
92-
*/
9369
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
9470
{
9571
if (str_ends_with($propertyName, '_count')) {
9672
return new ModelProperty($classReflection, IntegerRangeType::createAllGreaterThanOrEqualTo(0), new NeverType(), false);
9773
}
9874

99-
$method = $classReflection->getMethod($propertyName, new OutOfClassScope());
100-
101-
$returnType = $method->getVariants()[0]->getReturnType();
102-
103-
$relatedModel = $returnType->getTemplateType(Relation::class, 'TRelatedModel');
104-
105-
if ($relatedModel->getObjectClassNames() === []) {
106-
$relatedModelClassNames = [Model::class];
107-
} else {
108-
$relatedModelClassNames = $relatedModel->getObjectClassNames();
109-
}
75+
$returnType = $classReflection->getMethod($propertyName, new OutOfClassScope())
76+
->getVariants()[0]
77+
->getReturnType();
11078

111-
$relationType = TypeTraverser::map($returnType, function (Type $type, callable $traverse) use ($relatedModelClassNames, $relatedModel) {
79+
$relationType = TypeTraverser::map($returnType, static function (Type $type, callable $traverse): Type {
11280
if ($type instanceof UnionType || $type instanceof IntersectionType) {
11381
return $traverse($type);
11482
}
11583

116-
if ($type->getObjectClassNames() === []) {
117-
return $traverse($type);
118-
}
119-
120-
if ($type instanceof GenericObjectType) {
121-
$relatedModel = $type->getTypes()[0];
122-
$relatedModelClassNames = $relatedModel->getObjectClassNames();
123-
}
124-
125-
if (
126-
(new ObjectType(BelongsToMany::class))->isSuperTypeOf($type)->yes()
127-
|| (new ObjectType(HasMany::class))->isSuperTypeOf($type)->yes()
128-
|| (
129-
(new ObjectType(HasManyThrough::class))->isSuperTypeOf($type)->yes()
130-
// HasOneThrough extends HasManyThrough
131-
&& ! (new ObjectType(HasOneThrough::class))->isSuperTypeOf($type)->yes()
132-
)
133-
|| (new ObjectType(MorphMany::class))->isSuperTypeOf($type)->yes()
134-
|| (new ObjectType(MorphToMany::class))->isSuperTypeOf($type)->yes()
135-
|| Str::contains($type->getObjectClassNames()[0], 'Many') // fallback
136-
) {
137-
$types = [];
138-
139-
foreach ($relatedModelClassNames as $relatedModelClassName) {
140-
$types[] = $this->collectionHelper->determineCollectionClass($relatedModelClassName);
141-
}
142-
143-
if ($types !== []) {
144-
return TypeCombinator::union(...$types);
145-
}
146-
}
147-
148-
if (
149-
(new ObjectType(MorphTo::class))->isSuperTypeOf($type)->yes()
150-
|| Str::endsWith($type->getObjectClassNames()[0], 'MorphTo') // fallback
151-
) {
152-
// There was no generic type, or it was just Model
153-
// so we will return mixed to avoid errors.
154-
if ($relatedModel->getObjectClassNames()[0] === Model::class) {
155-
return new MixedType();
156-
}
157-
158-
$types = [];
159-
160-
foreach ($relatedModelClassNames as $relatedModelClassName) {
161-
$types[] = new ObjectType($relatedModelClassName);
162-
}
163-
164-
if ($types !== []) {
165-
return TypeCombinator::union(...$types);
166-
}
84+
if (! (new ObjectType(Relation::class))->isSuperTypeOf($type)->yes()) {
85+
return $type;
16786
}
16887

169-
return new UnionType([
170-
$relatedModel,
171-
new NullType(),
172-
]);
88+
return $type->getTemplateType(Relation::class, 'TResult');
17389
});
17490

91+
$relationType = $this->collectionHelper->replaceCollectionsInType($relationType);
92+
17593
return new ModelProperty($classReflection, $relationType, new NeverType(), false);
17694
}
17795
}

tests/Type/data/model-properties-relations.php

+2-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use App\Account;
66
use App\User;
7-
use Illuminate\Database\Eloquent\Collection;
87
use Illuminate\Database\Eloquent\Model;
98
use Illuminate\Database\Eloquent\Relations\BelongsTo;
109
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -98,13 +97,12 @@ function test(Foo $foo, Bar $bar, Account $account): void
9897
assertType('Illuminate\Database\Eloquent\Collection<int, ModelPropertiesRelations\Bar>', $foo->hasManyThroughRelation);
9998
assertType('ModelPropertiesRelations\Baz|null', $foo->hasOneThroughRelation);
10099
assertType('ModelPropertiesRelations\Foo', $bar->belongsToRelation);
101-
assertType('mixed', $bar->morphToRelation);
102-
assertType('App\Account|App\User', $bar->morphToUnionRelation);
100+
assertType('Illuminate\Database\Eloquent\Model|null', $bar->morphToRelation);
101+
assertType('App\Account|App\User|null', $bar->morphToUnionRelation);
103102
assertType('ModelPropertiesRelations\Bar|null', $foo->hasManyRelation->first());
104103
assertType('ModelPropertiesRelations\Bar|null', $foo->hasManyRelation()->find(1));
105104
assertType('App\User|null', $account->ownerRelation);
106105
assertType('Illuminate\Database\Eloquent\Collection<int, ModelPropertiesRelations\Bar>|ModelPropertiesRelations\Baz|null', $foo->relationReturningUnion);
107106
assertType('Illuminate\Database\Eloquent\Collection<int, ModelPropertiesRelations\Bar>|ModelPropertiesRelations\Baz|null', $foo->relationReturningUnion2);
108107
assertType('Illuminate\Database\Eloquent\Collection<int, ModelPropertiesRelations\Foo>', $foo->ancestors);
109108
}
110-

tests/Type/data/model-relations.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ function test(
229229

230230
assertType('Illuminate\Database\Eloquent\Relations\MorphTo<Illuminate\Database\Eloquent\Model, App\Comment>', $comment->commentable());
231231
assertType('Illuminate\Database\Eloquent\Model|null', $comment->commentable()->getResults());
232-
assertType('mixed', $comment->commentable);
232+
assertType('Illuminate\Database\Eloquent\Model|null', $comment->commentable);
233233
assertType('Illuminate\Database\Eloquent\Collection<int, App\Comment>', $comment->commentable()->getEager());
234234
assertType('Illuminate\Database\Eloquent\Model', $comment->commentable()->createModelByType('foo'));
235235
assertType('App\Comment', $comment->commentable()->associate(new Post()));

0 commit comments

Comments
 (0)