diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c705ef3e..4b2391b6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed * No return type should be enforced for closure of tap helper +* Ensure the model extensions considers PHPDoc `@property` tags from ancestors, not just the model class itself ### Changed * Improved return type for Collection::first, last, get, pull when giving a default value. diff --git a/src/Properties/ModelPropertyExtension.php b/src/Properties/ModelPropertyExtension.php index 0507a96c8..2743c5160 100644 --- a/src/Properties/ModelPropertyExtension.php +++ b/src/Properties/ModelPropertyExtension.php @@ -7,6 +7,7 @@ use ArrayObject; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; +use NunoMaduro\Larastan\Reflection\ReflectionHelper; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\PropertiesClassReflectionExtension; @@ -19,7 +20,7 @@ */ final class ModelPropertyExtension implements PropertiesClassReflectionExtension { - /** @var SchemaTable[] */ + /** @var array */ private $tables = []; /** @var TypeStringResolver */ @@ -51,7 +52,7 @@ public function hasProperty(ClassReflection $classReflection, string $propertyNa return false; } - if (array_key_exists($propertyName, $classReflection->getPropertyTags())) { + if (ReflectionHelper::hasPropertyTag($classReflection, $propertyName)) { return false; } diff --git a/src/Properties/ModelRelationsExtension.php b/src/Properties/ModelRelationsExtension.php index ff9449675..c131810f4 100644 --- a/src/Properties/ModelRelationsExtension.php +++ b/src/Properties/ModelRelationsExtension.php @@ -9,6 +9,7 @@ use Illuminate\Support\Str; use NunoMaduro\Larastan\Concerns; use NunoMaduro\Larastan\Methods\BuilderHelper; +use NunoMaduro\Larastan\Reflection\ReflectionHelper; use NunoMaduro\Larastan\Types\RelationParserHelper; use PHPStan\Analyser\OutOfClassScope; use PHPStan\Reflection\ClassReflection; @@ -49,7 +50,7 @@ public function hasProperty(ClassReflection $classReflection, string $propertyNa return false; } - if (array_key_exists($propertyName, $classReflection->getPropertyTags())) { + if (ReflectionHelper::hasPropertyTag($classReflection, $propertyName)) { return false; } diff --git a/src/Reflection/ReflectionHelper.php b/src/Reflection/ReflectionHelper.php new file mode 100644 index 000000000..de2a78911 --- /dev/null +++ b/src/Reflection/ReflectionHelper.php @@ -0,0 +1,28 @@ +getPropertyTags())) { + return true; + } + + foreach ($classReflection->getAncestors() as $ancestor) { + if (array_key_exists($propertyName, $ancestor->getPropertyTags())) { + return true; + } + } + + return false; + } +} diff --git a/tests/Features/Models/Relations.php b/tests/Features/Models/Relations.php index 6fba7c268..63d77705c 100644 --- a/tests/Features/Models/Relations.php +++ b/tests/Features/Models/Relations.php @@ -15,6 +15,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphToMany; use function PHPStan\Testing\assertType; @@ -232,6 +233,26 @@ public function testBelongsToManyCreateReturnsCorrectModel(User $user): Post return $user->posts()->create(); } + + public function testNullableUser(ExtendsModelWithPropertyAnnotations $model): bool + { + return $model->nullableUser === null; + } + + public function testNonNullableUser(ExtendsModelWithPropertyAnnotations $model): User + { + return $model->nonNullableUser; + } + + public function testNullableFoo(ExtendsModelWithPropertyAnnotations $model): bool + { + return $model->nullableFoo === null; + } + + public function testNonNullableFoo(ExtendsModelWithPropertyAnnotations $model): string + { + return $model->nonNullableFoo; + } } /** @@ -258,6 +279,39 @@ public function relation(): HasMany } } +/** + * @property-read User|null $nullableUser + * @property-read User $nonNullableUser + * @property-read string|null $nullableFoo + * @property-read string $nonNullableFoo + */ +class ModelWithPropertyAnnotations extends Model +{ + public function nullableUser(): HasOne + { + return $this->hasOne(User::class); + } + + public function nonNullableUser(): HasOne + { + return $this->hasOne(User::class); + } + + public function getNullableFooAttribute(): ?string + { + return rand() ? 'foo' : null; + } + + public function getNonNullableFooAttribute(): string + { + return 'foo'; + } +} + +class ExtendsModelWithPropertyAnnotations extends ModelWithPropertyAnnotations +{ +} + class Tag extends Model { /**