From 88883b21adac6bb9a773f3753d9d0d74802acd41 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 2 Nov 2022 15:31:51 +0100 Subject: [PATCH 01/12] feat(phpstan): pick up extended model relations typings Signed-off-by: Sami Mazouz --- php-packages/phpstan/extension.neon | 16 +++ .../phpstan/src/Extender/Extender.php | 73 ++++++++++ .../phpstan/src/Extender/FilesProvider.php | 47 +++++++ .../phpstan/src/Extender/MethodCall.php | 24 ++++ .../phpstan/src/Extender/Resolver.php | 130 +++++++++++++++++ .../src/Relations/ModelRelationsExtension.php | 86 ++++++++++++ .../phpstan/src/Relations/RelationMethod.php | 132 ++++++++++++++++++ .../src/Relations/RelationProperty.php | 110 +++++++++++++++ 8 files changed, 618 insertions(+) create mode 100644 php-packages/phpstan/src/Extender/Extender.php create mode 100644 php-packages/phpstan/src/Extender/FilesProvider.php create mode 100644 php-packages/phpstan/src/Extender/MethodCall.php create mode 100644 php-packages/phpstan/src/Extender/Resolver.php create mode 100644 php-packages/phpstan/src/Relations/ModelRelationsExtension.php create mode 100644 php-packages/phpstan/src/Relations/RelationMethod.php create mode 100644 php-packages/phpstan/src/Relations/RelationProperty.php diff --git a/php-packages/phpstan/extension.neon b/php-packages/phpstan/extension.neon index c8950dbaee..706ff65b94 100644 --- a/php-packages/phpstan/extension.neon +++ b/php-packages/phpstan/extension.neon @@ -15,3 +15,19 @@ parameters: - stubs/Illuminate/Contracts/Filesystem/Factory.stub - stubs/Illuminate/Contracts/Filesystem/Cloud.stub - stubs/Illuminate/Contracts/Filesystem/Filesystem.stub + +services: + - + class: Flarum\PHPStan\Relations\ModelRelationsExtension + tags: + - phpstan.broker.methodsClassReflectionExtension + - phpstan.broker.propertiesClassReflectionExtension + - + class: Flarum\PHPStan\Extender\FilesProvider + arguments: + - %paths% + - + class: Flarum\PHPStan\Extender\Resolver + arguments: + - @Flarum\PHPStan\Extender\FilesProvider + - @defaultAnalysisParser diff --git a/php-packages/phpstan/src/Extender/Extender.php b/php-packages/phpstan/src/Extender/Extender.php new file mode 100644 index 0000000000..61e351d186 --- /dev/null +++ b/php-packages/phpstan/src/Extender/Extender.php @@ -0,0 +1,73 @@ +qualifiedClassName = $qualifiedClassName; + $this->constructorArguments = $constructorArguments; + $this->methodCalls = $methodCalls; + } + + public function isExtender(string $className): bool + { + return $this->qualifiedClassName === "Flarum\\Extend\\$className"; + } + + public function extends(...$args): bool + { + foreach ($this->constructorArguments as $index => $constructorArgument) { + $string = null; + + switch (get_class($constructorArgument)) { + case Expr\ClassConstFetch::class: + $string = $constructorArgument->class->toString(); + break; + case Scalar\String_::class: + $string = $constructorArgument->value; + break; + default: + $string = $constructorArgument; + } + + if ($string !== $args[$index]) { + return false; + } + } + + return true; + } + + /** @return MethodCall[] */ + public function findMethodCalls(string ...$methods): array + { + $methodCalls = []; + + foreach ($this->methodCalls as $methodCall) { + if (in_array($methodCall->methodName, $methods)) { + $methodCalls[] = $methodCall; + } + } + + return $methodCalls; + } +} diff --git a/php-packages/phpstan/src/Extender/FilesProvider.php b/php-packages/phpstan/src/Extender/FilesProvider.php new file mode 100644 index 0000000000..d5f66bafba --- /dev/null +++ b/php-packages/phpstan/src/Extender/FilesProvider.php @@ -0,0 +1,47 @@ +paths = $paths; + } + + public function getExtenderFiles(): array + { + if ($this->cachedExtenderFiles === null) { + $this->cachedExtenderFiles = $this->findExtenderFiles(); + } + + return $this->cachedExtenderFiles; + } + + private function findExtenderFiles(): array + { + $extenderFiles = []; + + foreach ($this->paths as $path) { + $extenderFile = str_replace('src', 'extend.php', $path); + + if (file_exists($extenderFile)) { + $extenderFiles[] = $extenderFile; + } + } + + return $extenderFiles; + } +} diff --git a/php-packages/phpstan/src/Extender/MethodCall.php b/php-packages/phpstan/src/Extender/MethodCall.php new file mode 100644 index 0000000000..0d2f196898 --- /dev/null +++ b/php-packages/phpstan/src/Extender/MethodCall.php @@ -0,0 +1,24 @@ +methodName = $methodName; + $this->arguments = $arguments; + } +} diff --git a/php-packages/phpstan/src/Extender/Resolver.php b/php-packages/phpstan/src/Extender/Resolver.php new file mode 100644 index 0000000000..86f1188a00 --- /dev/null +++ b/php-packages/phpstan/src/Extender/Resolver.php @@ -0,0 +1,130 @@ +extenderFilesProvider = $extenderFilesProvider; + $this->parser = $parser; + } + + public function getExtenders(): array + { + if ($this->cachedExtenders) { + return $this->cachedExtenders; + } + + return $this->cachedExtenders = $this->resolveExtenders(); + } + + public function getExtendersFor(string $extenderClass, ...$args): array + { + $extenders = []; + + foreach ($this->getExtenders() as $extender) { + if ($extender->isExtender($extenderClass)) { + $extenders[] = $extender; + } + } + + return $extenders; + } + + private function resolveExtenders(): array + { + $extenders = []; + + foreach ($this->extenderFilesProvider->getExtenderFiles() as $extenderFile) { + $extenders = array_merge($extenders, $this->resolveExtendersFromFile($extenderFile)); + } + + return $extenders; + } + + /** + * @throws ParserErrorsException + * @throws \Exception + */ + private function resolveExtendersFromFile($extenderFile) + { + /** @var Extender[] $extenders */ + $extenders = []; + + $statements = $this->parser->parseFile($extenderFile); + + foreach ($statements as $statement) { + if ($statement instanceof Return_) { + $expression = $statement->expr; + + if ($expression instanceof Array_) { + foreach ($expression->items as $item) { + if ($item->value instanceof MethodCall) { + $extenders[] = $this->resolveExtender($item->value); + } + } + } + } + } + + return $extenders; + } + + private function resolveExtenderNew(New_ $var, array $methodCalls = []): Extender + { + return new Extender($var->class->toString(), array_map(function (Arg $arg) { + $arg->value->setAttributes([]); + return $arg->value; + }, $var->args), $methodCalls); + } + + private function resolveMethod(MethodCall $var): ExtenderMethodCall + { + return new ExtenderMethodCall($var->name->toString(), array_map(function (Arg $arg) { + $arg->value->setAttributes([]); + return $arg->value; + }, $var->args)); + } + + private function resolveExtender(MethodCall $value): Extender + { + $methodStack = [$this->resolveMethod($value)]; + + while ($value->var instanceof MethodCall) { + $methodStack[] = $this->resolveMethod($value->var); + $value = $value->var; + } + + $methodStack = array_reverse($methodStack); + + if (! $value->var instanceof New_) { + throw new \Exception('Unable to resolve extender for ' . get_class($value->var)); + } + + return $this->resolveExtenderNew($value->var, $methodStack); + } +} diff --git a/php-packages/phpstan/src/Relations/ModelRelationsExtension.php b/php-packages/phpstan/src/Relations/ModelRelationsExtension.php new file mode 100644 index 0000000000..737c383203 --- /dev/null +++ b/php-packages/phpstan/src/Relations/ModelRelationsExtension.php @@ -0,0 +1,86 @@ +extendersResolver = $extendersResolver; + } + + public function hasMethod(ClassReflection $classReflection, string $methodName): bool + { + return $this->findRelationMethod($classReflection, $methodName) !== null; + } + + public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection + { + return $this->resolveRelationMethod($this->findRelationMethod($classReflection, $methodName), $classReflection); + } + + public function hasProperty(ClassReflection $classReflection, string $propertyName): bool + { + return $this->findRelationMethod($classReflection, $propertyName) !== null; + } + + public function getProperty(ClassReflection $classReflection, string $propertyName): \PHPStan\Reflection\PropertyReflection + { + return $this->resolveRelationProperty($this->findRelationMethod($classReflection, $propertyName), $classReflection); + } + + private function findRelationMethod(ClassReflection $classReflection, string $methodName): ?MethodCall + { + foreach ($this->extendersResolver->getExtenders() as $extender) { + if (! $extender->isExtender('Model')) { + continue; + } + + foreach (array_merge([$classReflection->getName()], $classReflection->getParentClassesNames()) as $className) { + if ($className === 'Flarum\Database\AbstractModel') { + break; + } + + if ($extender->extends($className)) { + if ($methodCalls = $extender->findMethodCalls('belongsTo', 'belongsToMany', 'hasMany', 'hasOne')) { + foreach ($methodCalls as $methodCall) { + if ($methodCall->arguments[0]->value === $methodName) { + return $methodCall; + } + } + } + } + } + } + + return null; + } + + private function resolveRelationMethod(MethodCall $methodCall, ClassReflection $classReflection): MethodReflection + { + return new RelationMethod($methodCall, $classReflection); + } + + private function resolveRelationProperty(MethodCall $methodCall, ClassReflection $classReflection): PropertyReflection + { + return new RelationProperty($methodCall, $classReflection); + } +} diff --git a/php-packages/phpstan/src/Relations/RelationMethod.php b/php-packages/phpstan/src/Relations/RelationMethod.php new file mode 100644 index 0000000000..cbe4884001 --- /dev/null +++ b/php-packages/phpstan/src/Relations/RelationMethod.php @@ -0,0 +1,132 @@ +methodCall = $methodCall; + $this->classReflection = $classReflection; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->classReflection; + } + + public function isStatic(): bool + { + return false; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function getDocComment(): ?string + { + return null; + } + + public function getName(): string + { + return $this->methodCall->arguments[0]->value; + } + + public function getPrototype(): ClassMemberReflection + { + return $this; + } + + public function getVariants(): array + { + $returnType = 'Illuminate\Database\Eloquent\Relations\Relation'; + + switch ($this->methodCall->methodName) { + case 'belongsTo': + $returnType = 'Illuminate\Database\Eloquent\Relations\BelongsTo'; + break; + case 'belongsToMany': + $returnType = 'Illuminate\Database\Eloquent\Relations\BelongsToMany'; + break; + case 'hasMany': + $returnType = 'Illuminate\Database\Eloquent\Relations\HasMany'; + break; + case 'hasOne': + $returnType = 'Illuminate\Database\Eloquent\Relations\HasOne'; + break; + } + + $relationTarget = $this->methodCall->arguments[1]->class->toString(); + + return [ + new FunctionVariant( + TemplateTypeMap::createEmpty(), + null, + [], + false, + new GenericObjectType($returnType, [new ObjectType($relationTarget)]) + ), + ]; + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getThrowType(): ?Type + { + return null; + } + + public function hasSideEffects(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } +} diff --git a/php-packages/phpstan/src/Relations/RelationProperty.php b/php-packages/phpstan/src/Relations/RelationProperty.php new file mode 100644 index 0000000000..6a6440ab4e --- /dev/null +++ b/php-packages/phpstan/src/Relations/RelationProperty.php @@ -0,0 +1,110 @@ +methodCall = $methodCall; + $this->classReflection = $classReflection; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->classReflection; + } + + public function isStatic(): bool + { + return false; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function getDocComment(): ?string + { + return null; + } + + public function getReadableType(): Type + { + switch ($this->methodCall->methodName) { + case 'hasMany': + case 'belongsToMany': + return new GenericObjectType(Collection::class, [new ObjectType($this->methodCall->arguments[1]->class->toString())]); + + case 'hasOne': + case 'belongsTo': + return new ObjectType($this->methodCall->arguments[1]->class->toString()); + + default: + throw new Exception('Unknown relationship type for relation: ' . $this->methodCall->methodName); + } + } + + public function getWritableType(): Type + { + return $this->getReadableType(); + } + + public function canChangeTypeAfterAssignment(): bool + { + return false; + } + + public function isReadable(): bool + { + return true; + } + + public function isWritable(): bool + { + return true; + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } +} From c952bc6e31deb42f3a5b0506595fadf16d33d995 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 2 Nov 2022 18:07:39 +0100 Subject: [PATCH 02/12] feat(phpstan): pick up extended model date attributes Signed-off-by: Sami Mazouz --- php-packages/phpstan/extension.neon | 4 + .../src/Attributes/DateAttributeProperty.php | 91 +++++++++++++++++++ .../ModelDateAttributesExtension.php | 62 +++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 php-packages/phpstan/src/Attributes/DateAttributeProperty.php create mode 100644 php-packages/phpstan/src/Attributes/ModelDateAttributesExtension.php diff --git a/php-packages/phpstan/extension.neon b/php-packages/phpstan/extension.neon index 706ff65b94..c355591dc9 100644 --- a/php-packages/phpstan/extension.neon +++ b/php-packages/phpstan/extension.neon @@ -22,6 +22,10 @@ services: tags: - phpstan.broker.methodsClassReflectionExtension - phpstan.broker.propertiesClassReflectionExtension + - + class: Flarum\PHPStan\Attributes\ModelDateAttributesExtension + tags: + - phpstan.broker.propertiesClassReflectionExtension - class: Flarum\PHPStan\Extender\FilesProvider arguments: diff --git a/php-packages/phpstan/src/Attributes/DateAttributeProperty.php b/php-packages/phpstan/src/Attributes/DateAttributeProperty.php new file mode 100644 index 0000000000..21882e519a --- /dev/null +++ b/php-packages/phpstan/src/Attributes/DateAttributeProperty.php @@ -0,0 +1,91 @@ +classReflection = $classReflection; + } + + public function getDeclaringClass(): ClassReflection + { + return $this->classReflection; + } + + public function isStatic(): bool + { + return false; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function getDocComment(): ?string + { + return null; + } + + public function getReadableType(): Type + { + return new UnionType([ + new ObjectType(Carbon::class), + new NullType(), + ]); + } + + public function getWritableType(): Type + { + return $this->getReadableType(); + } + + public function canChangeTypeAfterAssignment(): bool + { + return false; + } + + public function isReadable(): bool + { + return true; + } + + public function isWritable(): bool + { + return true; + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } +} diff --git a/php-packages/phpstan/src/Attributes/ModelDateAttributesExtension.php b/php-packages/phpstan/src/Attributes/ModelDateAttributesExtension.php new file mode 100644 index 0000000000..68b0c13361 --- /dev/null +++ b/php-packages/phpstan/src/Attributes/ModelDateAttributesExtension.php @@ -0,0 +1,62 @@ +extendersResolver = $extendersResolver; + } + + public function hasProperty(ClassReflection $classReflection, string $propertyName): bool + { + return $this->findDateAttributeMethod($classReflection, $propertyName) !== null; + } + + public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection + { + return $this->resolveDateAttributeProperty($this->findDateAttributeMethod($classReflection, $propertyName), $classReflection); + } + + private function findDateAttributeMethod(ClassReflection $classReflection, string $propertyName): ?MethodCall + { + foreach ($this->extendersResolver->getExtenders() as $extender) { + if (! $extender->isExtender('Model')) { + continue; + } + + foreach (array_merge([$classReflection->getName()], $classReflection->getParentClassesNames()) as $className) { + if ($className === 'Flarum\Database\AbstractModel') { + break; + } + + if ($extender->extends($className)) { + if ($methodCalls = $extender->findMethodCalls('dateAttribute')) { + foreach ($methodCalls as $methodCall) { + if ($methodCall->arguments[0]->value === $propertyName) { + return $methodCall; + } + } + } + } + } + } + + return null; + } + + private function resolveDateAttributeProperty(MethodCall $methodCall, ClassReflection $classReflection): PropertyReflection + { + return new DateAttributeProperty($classReflection); + } +} From a94cda667bb77416b40f64806dc0c5e3efc97e3f Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 2 Nov 2022 22:03:13 +0100 Subject: [PATCH 03/12] feat(core): introduce `castAttribute` extender Stops using `dates` as it's deprecated in laravel 8 Signed-off-by: Sami Mazouz --- framework/core/src/Database/AbstractModel.php | 14 ++++---- framework/core/src/Extend/Model.php | 34 ++++++++++++++----- .../tests/integration/extenders/ModelTest.php | 22 ++++++------ 3 files changed, 43 insertions(+), 27 deletions(-) diff --git a/framework/core/src/Database/AbstractModel.php b/framework/core/src/Database/AbstractModel.php index f61c89c875..86e6ef995b 100644 --- a/framework/core/src/Database/AbstractModel.php +++ b/framework/core/src/Database/AbstractModel.php @@ -54,7 +54,7 @@ abstract class AbstractModel extends Eloquent /** * @internal */ - public static $dateAttributes = []; + public static $customCasts = []; /** * @internal @@ -100,19 +100,17 @@ public function __construct(array $attributes = []) } /** - * Get the attributes that should be converted to dates. - * - * @return array + * {@inheritdoc} */ - public function getDates() + public function getCasts() { - $dates = $this->dates; + $casts = parent::getCasts(); foreach (array_merge(array_reverse(class_parents($this)), [static::class]) as $class) { - $dates = array_merge($dates, Arr::get(static::$dateAttributes, $class, [])); + $casts = array_merge($casts, Arr::get(static::$customCasts, $class, [])); } - return $dates; + return $casts; } /** diff --git a/framework/core/src/Extend/Model.php b/framework/core/src/Extend/Model.php index 07b0d1b7e1..cf45a5e932 100644 --- a/framework/core/src/Extend/Model.php +++ b/framework/core/src/Extend/Model.php @@ -19,6 +19,7 @@ class Model implements ExtenderInterface { private $modelClass; private $customRelations = []; + private $casts = []; /** * @param string $modelClass: The ::class attribute of the model you are modifying. @@ -34,17 +35,25 @@ public function __construct(string $modelClass) * * @param string $attribute * @return self + * @deprecated use `castAttribute` instead. Will be removed in v2. */ public function dateAttribute(string $attribute): self { - Arr::set( - AbstractModel::$dateAttributes, - $this->modelClass, - array_merge( - Arr::get(AbstractModel::$dateAttributes, $this->modelClass, []), - [$attribute] - ) - ); + $this->castAttribute($attribute, 'datetime'); + + return $this; + } + + /** + * Add a custom attribute type cast. Should not be applied to non-extension attributes. + * + * @param string $attribute: The new attribute name. + * @param string $cast: The cast type. See https://laravel.com/docs/8.x/eloquent-mutators#attribute-casting + * @return self + */ + public function castAttribute(string $attribute, string $cast): self + { + $this->casts[$attribute] = $cast; return $this; } @@ -184,5 +193,14 @@ public function extend(Container $container, Extension $extension = null) foreach ($this->customRelations as $name => $callback) { Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", ContainerUtil::wrapCallback($callback, $container)); } + + Arr::set( + AbstractModel::$customCasts, + $this->modelClass, + array_merge( + Arr::get(AbstractModel::$customCasts, $this->modelClass, []), + $this->casts + ) + ); } } diff --git a/framework/core/tests/integration/extenders/ModelTest.php b/framework/core/tests/integration/extenders/ModelTest.php index a2228247e6..301778d994 100644 --- a/framework/core/tests/integration/extenders/ModelTest.php +++ b/framework/core/tests/integration/extenders/ModelTest.php @@ -375,64 +375,64 @@ public function custom_default_attribute_doesnt_work_if_set_on_unrelated_model() /** * @test */ - public function custom_date_attribute_doesnt_exist_by_default() + public function custom_cast_attribute_doesnt_exist_by_default() { $post = new Post; $this->app(); - $this->assertNotContains('custom', $post->getDates()); + $this->assertFalse($post->hasCast('custom')); } /** * @test */ - public function custom_date_attribute_can_be_set() + public function custom_cast_attribute_can_be_set() { $this->extend( (new Extend\Model(Post::class)) - ->dateAttribute('custom') + ->castAttribute('custom', 'datetime') ); $this->app(); $post = new Post; - $this->assertContains('custom', $post->getDates()); + $this->assertTrue($post->hasCast('custom', 'datetime')); } /** * @test */ - public function custom_date_attribute_is_inherited_to_child_classes() + public function custom_cast_attribute_is_inherited_to_child_classes() { $this->extend( (new Extend\Model(Post::class)) - ->dateAttribute('custom') + ->castAttribute('custom', 'boolean') ); $this->app(); $post = new CommentPost; - $this->assertContains('custom', $post->getDates()); + $this->assertTrue($post->hasCast('custom', 'boolean')); } /** * @test */ - public function custom_date_attribute_doesnt_work_if_set_on_unrelated_model() + public function custom_cast_attribute_doesnt_work_if_set_on_unrelated_model() { $this->extend( (new Extend\Model(Post::class)) - ->dateAttribute('custom') + ->castAttribute('custom', 'integer') ); $this->app(); $discussion = new Discussion; - $this->assertNotContains('custom', $discussion->getDates()); + $this->assertFalse($discussion->hasCast('custom', 'integer')); } } From bb9b6fa2837b8d0ab4b8c2a40d4ec5222121b16a Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 2 Nov 2022 22:13:21 +0100 Subject: [PATCH 04/12] feat(phpstan): pick up extended model attributes through casts Signed-off-by: Sami Mazouz --- php-packages/phpstan/extension.neon | 4 ++ ...buteProperty.php => AttributeProperty.php} | 12 ++-- .../Attributes/ModelCastAttributeProperty.php | 70 +++++++++++++++++++ .../ModelDateAttributesExtension.php | 9 ++- 4 files changed, 88 insertions(+), 7 deletions(-) rename php-packages/phpstan/src/Attributes/{DateAttributeProperty.php => AttributeProperty.php} (90%) create mode 100644 php-packages/phpstan/src/Attributes/ModelCastAttributeProperty.php diff --git a/php-packages/phpstan/extension.neon b/php-packages/phpstan/extension.neon index c355591dc9..fe89f810e3 100644 --- a/php-packages/phpstan/extension.neon +++ b/php-packages/phpstan/extension.neon @@ -26,6 +26,10 @@ services: class: Flarum\PHPStan\Attributes\ModelDateAttributesExtension tags: - phpstan.broker.propertiesClassReflectionExtension + - + class: Flarum\PHPStan\Attributes\ModelCastAttributeProperty + tags: + - phpstan.broker.propertiesClassReflectionExtension - class: Flarum\PHPStan\Extender\FilesProvider arguments: diff --git a/php-packages/phpstan/src/Attributes/DateAttributeProperty.php b/php-packages/phpstan/src/Attributes/AttributeProperty.php similarity index 90% rename from php-packages/phpstan/src/Attributes/DateAttributeProperty.php rename to php-packages/phpstan/src/Attributes/AttributeProperty.php index 21882e519a..d9ec894ad3 100644 --- a/php-packages/phpstan/src/Attributes/DateAttributeProperty.php +++ b/php-packages/phpstan/src/Attributes/AttributeProperty.php @@ -11,14 +11,17 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; -class DateAttributeProperty implements PropertyReflection +class AttributeProperty implements PropertyReflection { /** @var ClassReflection */ private $classReflection; + /** @var Type */ + private $type; - public function __construct(ClassReflection $classReflection) + public function __construct(ClassReflection $classReflection, Type $type) { $this->classReflection = $classReflection; + $this->type = $type; } public function getDeclaringClass(): ClassReflection @@ -48,10 +51,7 @@ public function getDocComment(): ?string public function getReadableType(): Type { - return new UnionType([ - new ObjectType(Carbon::class), - new NullType(), - ]); + return $this->type; } public function getWritableType(): Type diff --git a/php-packages/phpstan/src/Attributes/ModelCastAttributeProperty.php b/php-packages/phpstan/src/Attributes/ModelCastAttributeProperty.php new file mode 100644 index 0000000000..f83c7f9754 --- /dev/null +++ b/php-packages/phpstan/src/Attributes/ModelCastAttributeProperty.php @@ -0,0 +1,70 @@ +extendersResolver = $extendersResolver; + $this->typeStringResolver = $typeStringResolver; + } + + public function hasProperty(ClassReflection $classReflection, string $propertyName): bool + { + return $this->findCastAttributeMethod($classReflection, $propertyName) !== null; + } + + public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection + { + return $this->resolveCastAttributeProperty($this->findCastAttributeMethod($classReflection, $propertyName), $classReflection); + } + + private function findCastAttributeMethod(ClassReflection $classReflection, string $propertyName): ?MethodCall + { + foreach ($this->extendersResolver->getExtenders() as $extender) { + if (! $extender->isExtender('Model')) { + continue; + } + + foreach (array_merge([$classReflection->getName()], $classReflection->getParentClassesNames()) as $className) { + if ($className === 'Flarum\Database\AbstractModel') { + break; + } + + if ($extender->extends($className)) { + if ($methodCalls = $extender->findMethodCalls('castAttribute')) { + foreach ($methodCalls as $methodCall) { + if ($methodCall->arguments[0]->value === $propertyName) { + return $methodCall; + } + } + } + } + } + } + + return null; + } + + private function resolveCastAttributeProperty(?MethodCall $methodCall, ClassReflection $classReflection): PropertyReflection + { + return new AttributeProperty($classReflection, new UnionType([ + $this->typeStringResolver->resolve($methodCall->arguments[1]->value), + new NullType(), + ])); + } +} diff --git a/php-packages/phpstan/src/Attributes/ModelDateAttributesExtension.php b/php-packages/phpstan/src/Attributes/ModelDateAttributesExtension.php index 68b0c13361..9b1622818c 100644 --- a/php-packages/phpstan/src/Attributes/ModelDateAttributesExtension.php +++ b/php-packages/phpstan/src/Attributes/ModelDateAttributesExtension.php @@ -2,11 +2,15 @@ namespace Flarum\PHPStan\Attributes; +use Carbon\Carbon; use Flarum\PHPStan\Extender\MethodCall; use Flarum\PHPStan\Extender\Resolver; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\PropertiesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; +use PHPStan\Type\NullType; +use PHPStan\Type\ObjectType; +use PHPStan\Type\UnionType; class ModelDateAttributesExtension implements PropertiesClassReflectionExtension { @@ -57,6 +61,9 @@ private function findDateAttributeMethod(ClassReflection $classReflection, strin private function resolveDateAttributeProperty(MethodCall $methodCall, ClassReflection $classReflection): PropertyReflection { - return new DateAttributeProperty($classReflection); + return new AttributeProperty($classReflection, new UnionType([ + new ObjectType(Carbon::class), + new NullType(), + ])); } } From 9531e7ef9121fa4fb19616ab408145dba6ba275f Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Thu, 3 Nov 2022 14:06:45 +0100 Subject: [PATCH 05/12] fix: extenders not resolved when declared namespace Signed-off-by: Sami Mazouz --- php-packages/phpstan/src/Extender/Resolver.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/php-packages/phpstan/src/Extender/Resolver.php b/php-packages/phpstan/src/Extender/Resolver.php index 86f1188a00..968f0bfc8e 100644 --- a/php-packages/phpstan/src/Extender/Resolver.php +++ b/php-packages/phpstan/src/Extender/Resolver.php @@ -14,6 +14,7 @@ use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\New_; +use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Return_; use PHPStan\Parser\Parser; use PHPStan\Parser\ParserErrorsException; @@ -67,16 +68,21 @@ private function resolveExtenders(): array } /** + * @return Extender[] * @throws ParserErrorsException * @throws \Exception */ - private function resolveExtendersFromFile($extenderFile) + private function resolveExtendersFromFile($extenderFile): array { /** @var Extender[] $extenders */ $extenders = []; $statements = $this->parser->parseFile($extenderFile); + if ($statements[0] instanceof Namespace_) { + $statements = $statements[0]->stmts; + } + foreach ($statements as $statement) { if ($statement instanceof Return_) { $expression = $statement->expr; From 0468f6e2e100220a155a2d2cfeb006cdf8d63323 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Thu, 3 Nov 2022 18:44:55 +0100 Subject: [PATCH 06/12] fix(phpstan): new model attributes are always nullable Signed-off-by: Sami Mazouz --- .../Attributes/ModelCastAttributeProperty.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/php-packages/phpstan/src/Attributes/ModelCastAttributeProperty.php b/php-packages/phpstan/src/Attributes/ModelCastAttributeProperty.php index f83c7f9754..67bf009112 100644 --- a/php-packages/phpstan/src/Attributes/ModelCastAttributeProperty.php +++ b/php-packages/phpstan/src/Attributes/ModelCastAttributeProperty.php @@ -2,12 +2,14 @@ namespace Flarum\PHPStan\Attributes; +use Carbon\Carbon; use Flarum\PHPStan\Extender\MethodCall; use Flarum\PHPStan\Extender\Resolver; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\PropertiesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectType; use PHPStan\Type\UnionType; class ModelCastAttributeProperty implements PropertiesClassReflectionExtension @@ -60,11 +62,18 @@ private function findCastAttributeMethod(ClassReflection $classReflection, strin return null; } - private function resolveCastAttributeProperty(?MethodCall $methodCall, ClassReflection $classReflection): PropertyReflection + private function resolveCastAttributeProperty(MethodCall $methodCall, ClassReflection $classReflection): PropertyReflection { - return new AttributeProperty($classReflection, new UnionType([ - $this->typeStringResolver->resolve($methodCall->arguments[1]->value), - new NullType(), - ])); + $typeName = $methodCall->arguments[1]->value; + $type = $this->typeStringResolver->resolve("$typeName|null"); + + if (str_contains($typeName, 'date') || $typeName === 'timestamp') { + $type = new UnionType([ + new ObjectType(Carbon::class), + new NullType(), + ]); + } + + return new AttributeProperty($classReflection, $type); } } From 7d4f021377b8b79597cde5d3409f4f154a75fd48 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Thu, 3 Nov 2022 18:49:02 +0100 Subject: [PATCH 07/12] chore(phpstan): add helpful cache clearing command Signed-off-by: Sami Mazouz --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e425a72bd6..efa4326c3e 100644 --- a/composer.json +++ b/composer.json @@ -180,7 +180,8 @@ } }, "scripts": { - "analyse:phpstan": "phpstan analyse" + "analyse:phpstan": "phpstan analyse", + "clear-cache:phpstan": "phpstan clear-result-cache" }, "scripts-descriptions": { "analyse:phpstan": "Run static analysis" From 52d805eee82eb13afb009a132135acaa9e89e9c3 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 3 Nov 2022 18:39:57 +0000 Subject: [PATCH 08/12] Apply fixes from StyleCI --- .../phpstan/src/Attributes/AttributeProperty.php | 11 +++++++---- .../src/Attributes/ModelCastAttributeProperty.php | 7 +++++++ .../src/Attributes/ModelDateAttributesExtension.php | 7 +++++++ php-packages/phpstan/src/Extender/Resolver.php | 4 +++- .../phpstan/src/Relations/RelationProperty.php | 2 +- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/php-packages/phpstan/src/Attributes/AttributeProperty.php b/php-packages/phpstan/src/Attributes/AttributeProperty.php index d9ec894ad3..d30f1d2fd9 100644 --- a/php-packages/phpstan/src/Attributes/AttributeProperty.php +++ b/php-packages/phpstan/src/Attributes/AttributeProperty.php @@ -1,15 +1,18 @@ class->toString(), array_map(function (Arg $arg) { $arg->value->setAttributes([]); + return $arg->value; }, $var->args), $methodCalls); } @@ -112,6 +113,7 @@ private function resolveMethod(MethodCall $var): ExtenderMethodCall { return new ExtenderMethodCall($var->name->toString(), array_map(function (Arg $arg) { $arg->value->setAttributes([]); + return $arg->value; }, $var->args)); } @@ -128,7 +130,7 @@ private function resolveExtender(MethodCall $value): Extender $methodStack = array_reverse($methodStack); if (! $value->var instanceof New_) { - throw new \Exception('Unable to resolve extender for ' . get_class($value->var)); + throw new \Exception('Unable to resolve extender for '.get_class($value->var)); } return $this->resolveExtenderNew($value->var, $methodStack); diff --git a/php-packages/phpstan/src/Relations/RelationProperty.php b/php-packages/phpstan/src/Relations/RelationProperty.php index 6a6440ab4e..108ceee570 100644 --- a/php-packages/phpstan/src/Relations/RelationProperty.php +++ b/php-packages/phpstan/src/Relations/RelationProperty.php @@ -69,7 +69,7 @@ public function getReadableType(): Type return new ObjectType($this->methodCall->arguments[1]->class->toString()); default: - throw new Exception('Unknown relationship type for relation: ' . $this->methodCall->methodName); + throw new Exception('Unknown relationship type for relation: '.$this->methodCall->methodName); } } From 2e464d33ef31b0860f5c043c1a6c85eb57ef2f65 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Fri, 4 Nov 2022 11:11:22 +0100 Subject: [PATCH 09/12] chore: improve extend files provider logic Signed-off-by: Sami Mazouz --- php-packages/phpstan/src/Extender/FilesProvider.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/php-packages/phpstan/src/Extender/FilesProvider.php b/php-packages/phpstan/src/Extender/FilesProvider.php index d5f66bafba..4b7b4e58d3 100644 --- a/php-packages/phpstan/src/Extender/FilesProvider.php +++ b/php-packages/phpstan/src/Extender/FilesProvider.php @@ -35,10 +35,8 @@ private function findExtenderFiles(): array $extenderFiles = []; foreach ($this->paths as $path) { - $extenderFile = str_replace('src', 'extend.php', $path); - - if (file_exists($extenderFile)) { - $extenderFiles[] = $extenderFile; + if (str_contains($path, 'extend.php') && file_exists($path)) { + $extenderFiles[] = $path; } } From 1ae334613de3004f13a673429e709f02df53f57d Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 20 Nov 2022 22:47:17 +0100 Subject: [PATCH 10/12] chore: rename `castAttribute` to just `cast` Signed-off-by: Sami Mazouz --- framework/core/src/Extend/Model.php | 4 ++-- framework/core/tests/integration/extenders/ModelTest.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/framework/core/src/Extend/Model.php b/framework/core/src/Extend/Model.php index cf45a5e932..ba36ef6aea 100644 --- a/framework/core/src/Extend/Model.php +++ b/framework/core/src/Extend/Model.php @@ -39,7 +39,7 @@ public function __construct(string $modelClass) */ public function dateAttribute(string $attribute): self { - $this->castAttribute($attribute, 'datetime'); + $this->cast($attribute, 'datetime'); return $this; } @@ -51,7 +51,7 @@ public function dateAttribute(string $attribute): self * @param string $cast: The cast type. See https://laravel.com/docs/8.x/eloquent-mutators#attribute-casting * @return self */ - public function castAttribute(string $attribute, string $cast): self + public function cast(string $attribute, string $cast): self { $this->casts[$attribute] = $cast; diff --git a/framework/core/tests/integration/extenders/ModelTest.php b/framework/core/tests/integration/extenders/ModelTest.php index 301778d994..a0580e6f73 100644 --- a/framework/core/tests/integration/extenders/ModelTest.php +++ b/framework/core/tests/integration/extenders/ModelTest.php @@ -391,7 +391,7 @@ public function custom_cast_attribute_can_be_set() { $this->extend( (new Extend\Model(Post::class)) - ->castAttribute('custom', 'datetime') + ->cast('custom', 'datetime') ); $this->app(); @@ -408,7 +408,7 @@ public function custom_cast_attribute_is_inherited_to_child_classes() { $this->extend( (new Extend\Model(Post::class)) - ->castAttribute('custom', 'boolean') + ->cast('custom', 'boolean') ); $this->app(); @@ -425,7 +425,7 @@ public function custom_cast_attribute_doesnt_work_if_set_on_unrelated_model() { $this->extend( (new Extend\Model(Post::class)) - ->castAttribute('custom', 'integer') + ->cast('custom', 'integer') ); $this->app(); From ffaf72216bfd2ea7320ff8c4367ca7cacd5dbb59 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Mon, 21 Nov 2022 18:29:27 +0100 Subject: [PATCH 11/12] chore: update phpstan package to detect `cast` method Signed-off-by: Sami Mazouz --- php-packages/phpstan/extension.neon | 2 +- ...ibuteProperty.php => ModelCastAttributeExtension.php} | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) rename php-packages/phpstan/src/Attributes/{ModelCastAttributeProperty.php => ModelCastAttributeExtension.php} (91%) diff --git a/php-packages/phpstan/extension.neon b/php-packages/phpstan/extension.neon index fe89f810e3..7de0093317 100644 --- a/php-packages/phpstan/extension.neon +++ b/php-packages/phpstan/extension.neon @@ -27,7 +27,7 @@ services: tags: - phpstan.broker.propertiesClassReflectionExtension - - class: Flarum\PHPStan\Attributes\ModelCastAttributeProperty + class: Flarum\PHPStan\Attributes\ModelCastAttributeExtension tags: - phpstan.broker.propertiesClassReflectionExtension - diff --git a/php-packages/phpstan/src/Attributes/ModelCastAttributeProperty.php b/php-packages/phpstan/src/Attributes/ModelCastAttributeExtension.php similarity index 91% rename from php-packages/phpstan/src/Attributes/ModelCastAttributeProperty.php rename to php-packages/phpstan/src/Attributes/ModelCastAttributeExtension.php index 6db868e0a5..49c3c8b2ab 100644 --- a/php-packages/phpstan/src/Attributes/ModelCastAttributeProperty.php +++ b/php-packages/phpstan/src/Attributes/ModelCastAttributeExtension.php @@ -12,6 +12,7 @@ use Carbon\Carbon; use Flarum\PHPStan\Extender\MethodCall; use Flarum\PHPStan\Extender\Resolver; +use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\PropertiesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; @@ -19,14 +20,14 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\UnionType; -class ModelCastAttributeProperty implements PropertiesClassReflectionExtension +class ModelCastAttributeExtension implements PropertiesClassReflectionExtension { /** @var Resolver */ private $extendersResolver; - /** @var \PHPStan\PhpDoc\TypeStringResolver */ + /** @var TypeStringResolver */ private $typeStringResolver; - public function __construct(Resolver $extendersResolver, \PHPStan\PhpDoc\TypeStringResolver $typeStringResolver) + public function __construct(Resolver $extendersResolver, TypeStringResolver $typeStringResolver) { $this->extendersResolver = $extendersResolver; $this->typeStringResolver = $typeStringResolver; @@ -55,7 +56,7 @@ private function findCastAttributeMethod(ClassReflection $classReflection, strin } if ($extender->extends($className)) { - if ($methodCalls = $extender->findMethodCalls('castAttribute')) { + if ($methodCalls = $extender->findMethodCalls('cast')) { foreach ($methodCalls as $methodCall) { if ($methodCall->arguments[0]->value === $propertyName) { return $methodCall; From ba01e3120ff225702d65486b95f9661c3b18b6b1 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Fri, 13 Jan 2023 20:59:38 +0100 Subject: [PATCH 12/12] Update framework/core/src/Extend/Model.php --- framework/core/src/Extend/Model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/core/src/Extend/Model.php b/framework/core/src/Extend/Model.php index ba36ef6aea..3b26fe729b 100644 --- a/framework/core/src/Extend/Model.php +++ b/framework/core/src/Extend/Model.php @@ -35,7 +35,7 @@ public function __construct(string $modelClass) * * @param string $attribute * @return self - * @deprecated use `castAttribute` instead. Will be removed in v2. + * @deprecated use `cast` instead. Will be removed in v2. */ public function dateAttribute(string $attribute): self {