From 5fe3cfd8376ae74d2e91fc12e8619bd958aa9209 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 15 Jan 2023 15:25:13 +0100 Subject: [PATCH] feat(phpstan): foundation for usage in extensions (#3666) * feat(phpstan): pick up extended model relations typings * feat(phpstan): pick up extended model date attributes * feat(core): introduce `castAttribute` extender Stops using `dates` as it's deprecated in laravel 8 * feat(phpstan): pick up extended model attributes through casts * fix: extenders not resolved when declared namespace * fix(phpstan): new model attributes are always nullable * chore(phpstan): add helpful cache clearing command * Apply fixes from StyleCI * chore: improve extend files provider logic * chore: rename `castAttribute` to just `cast` * chore: update phpstan package to detect `cast` method * Update framework/core/src/Extend/Model.php Signed-off-by: Sami Mazouz --- composer.json | 3 +- framework/core/src/Database/AbstractModel.php | 14 +- framework/core/src/Extend/Model.php | 34 ++++- .../tests/integration/extenders/ModelTest.php | 22 +-- php-packages/phpstan/extension.neon | 24 +++ .../src/Attributes/AttributeProperty.php | 94 ++++++++++++ .../ModelCastAttributeExtension.php | 87 +++++++++++ .../ModelDateAttributesExtension.php | 76 ++++++++++ .../phpstan/src/Extender/Extender.php | 73 +++++++++ .../phpstan/src/Extender/FilesProvider.php | 45 ++++++ .../phpstan/src/Extender/MethodCall.php | 24 +++ .../phpstan/src/Extender/Resolver.php | 138 ++++++++++++++++++ .../src/Relations/ModelRelationsExtension.php | 86 +++++++++++ .../phpstan/src/Relations/RelationMethod.php | 132 +++++++++++++++++ .../src/Relations/RelationProperty.php | 110 ++++++++++++++ 15 files changed, 934 insertions(+), 28 deletions(-) create mode 100644 php-packages/phpstan/src/Attributes/AttributeProperty.php create mode 100644 php-packages/phpstan/src/Attributes/ModelCastAttributeExtension.php create mode 100644 php-packages/phpstan/src/Attributes/ModelDateAttributesExtension.php 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/composer.json b/composer.json index e425a72bd..efa4326c3 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" diff --git a/framework/core/src/Database/AbstractModel.php b/framework/core/src/Database/AbstractModel.php index f61c89c87..86e6ef995 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 @@ abstract class AbstractModel extends Eloquent } /** - * 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 07b0d1b7e..3b26fe729 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 @@ class Model implements ExtenderInterface * * @param string $attribute * @return self + * @deprecated use `cast` 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->cast($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 cast(string $attribute, string $cast): self + { + $this->casts[$attribute] = $cast; return $this; } @@ -184,5 +193,14 @@ class Model implements ExtenderInterface 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 a2228247e..a0580e6f7 100644 --- a/framework/core/tests/integration/extenders/ModelTest.php +++ b/framework/core/tests/integration/extenders/ModelTest.php @@ -375,64 +375,64 @@ class ModelTest extends TestCase /** * @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') + ->cast('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') + ->cast('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') + ->cast('custom', 'integer') ); $this->app(); $discussion = new Discussion; - $this->assertNotContains('custom', $discussion->getDates()); + $this->assertFalse($discussion->hasCast('custom', 'integer')); } } diff --git a/php-packages/phpstan/extension.neon b/php-packages/phpstan/extension.neon index c8950dbae..7de009331 100644 --- a/php-packages/phpstan/extension.neon +++ b/php-packages/phpstan/extension.neon @@ -15,3 +15,27 @@ 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\Attributes\ModelDateAttributesExtension + tags: + - phpstan.broker.propertiesClassReflectionExtension + - + class: Flarum\PHPStan\Attributes\ModelCastAttributeExtension + tags: + - 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/Attributes/AttributeProperty.php b/php-packages/phpstan/src/Attributes/AttributeProperty.php new file mode 100644 index 000000000..d30f1d2fd --- /dev/null +++ b/php-packages/phpstan/src/Attributes/AttributeProperty.php @@ -0,0 +1,94 @@ +classReflection = $classReflection; + $this->type = $type; + } + + 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 $this->type; + } + + 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/ModelCastAttributeExtension.php b/php-packages/phpstan/src/Attributes/ModelCastAttributeExtension.php new file mode 100644 index 000000000..49c3c8b2a --- /dev/null +++ b/php-packages/phpstan/src/Attributes/ModelCastAttributeExtension.php @@ -0,0 +1,87 @@ +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('cast')) { + foreach ($methodCalls as $methodCall) { + if ($methodCall->arguments[0]->value === $propertyName) { + return $methodCall; + } + } + } + } + } + } + + return null; + } + + private function resolveCastAttributeProperty(MethodCall $methodCall, ClassReflection $classReflection): PropertyReflection + { + $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); + } +} diff --git a/php-packages/phpstan/src/Attributes/ModelDateAttributesExtension.php b/php-packages/phpstan/src/Attributes/ModelDateAttributesExtension.php new file mode 100644 index 000000000..469936667 --- /dev/null +++ b/php-packages/phpstan/src/Attributes/ModelDateAttributesExtension.php @@ -0,0 +1,76 @@ +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 AttributeProperty($classReflection, new UnionType([ + new ObjectType(Carbon::class), + new NullType(), + ])); + } +} diff --git a/php-packages/phpstan/src/Extender/Extender.php b/php-packages/phpstan/src/Extender/Extender.php new file mode 100644 index 000000000..61e351d18 --- /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 000000000..4b7b4e58d --- /dev/null +++ b/php-packages/phpstan/src/Extender/FilesProvider.php @@ -0,0 +1,45 @@ +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) { + if (str_contains($path, 'extend.php') && file_exists($path)) { + $extenderFiles[] = $path; + } + } + + 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 000000000..0d2f19689 --- /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 000000000..e1e3dcf39 --- /dev/null +++ b/php-packages/phpstan/src/Extender/Resolver.php @@ -0,0 +1,138 @@ +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; + } + + /** + * @return Extender[] + * @throws ParserErrorsException + * @throws \Exception + */ + 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; + + 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 000000000..737c38320 --- /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 000000000..cbe488400 --- /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 000000000..108ceee57 --- /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(); + } +}