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 <sychocouldy@gmail.com>
This commit is contained in:
Sami Mazouz 2023-01-15 15:25:13 +01:00 committed by GitHub
parent 2d2bf5c504
commit 5fe3cfd837
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 934 additions and 28 deletions

View File

@ -180,7 +180,8 @@
} }
}, },
"scripts": { "scripts": {
"analyse:phpstan": "phpstan analyse" "analyse:phpstan": "phpstan analyse",
"clear-cache:phpstan": "phpstan clear-result-cache"
}, },
"scripts-descriptions": { "scripts-descriptions": {
"analyse:phpstan": "Run static analysis" "analyse:phpstan": "Run static analysis"

View File

@ -54,7 +54,7 @@ abstract class AbstractModel extends Eloquent
/** /**
* @internal * @internal
*/ */
public static $dateAttributes = []; public static $customCasts = [];
/** /**
* @internal * @internal
@ -100,19 +100,17 @@ abstract class AbstractModel extends Eloquent
} }
/** /**
* Get the attributes that should be converted to dates. * {@inheritdoc}
*
* @return array
*/ */
public function getDates() public function getCasts()
{ {
$dates = $this->dates; $casts = parent::getCasts();
foreach (array_merge(array_reverse(class_parents($this)), [static::class]) as $class) { 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;
} }
/** /**

View File

@ -19,6 +19,7 @@ class Model implements ExtenderInterface
{ {
private $modelClass; private $modelClass;
private $customRelations = []; private $customRelations = [];
private $casts = [];
/** /**
* @param string $modelClass: The ::class attribute of the model you are modifying. * @param string $modelClass: The ::class attribute of the model you are modifying.
@ -34,17 +35,25 @@ class Model implements ExtenderInterface
* *
* @param string $attribute * @param string $attribute
* @return self * @return self
* @deprecated use `cast` instead. Will be removed in v2.
*/ */
public function dateAttribute(string $attribute): self public function dateAttribute(string $attribute): self
{ {
Arr::set( $this->cast($attribute, 'datetime');
AbstractModel::$dateAttributes,
$this->modelClass, return $this;
array_merge( }
Arr::get(AbstractModel::$dateAttributes, $this->modelClass, []),
[$attribute] /**
) * 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; return $this;
} }
@ -184,5 +193,14 @@ class Model implements ExtenderInterface
foreach ($this->customRelations as $name => $callback) { foreach ($this->customRelations as $name => $callback) {
Arr::set(AbstractModel::$customRelations, "$this->modelClass.$name", ContainerUtil::wrapCallback($callback, $container)); 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
)
);
} }
} }

View File

@ -375,64 +375,64 @@ class ModelTest extends TestCase
/** /**
* @test * @test
*/ */
public function custom_date_attribute_doesnt_exist_by_default() public function custom_cast_attribute_doesnt_exist_by_default()
{ {
$post = new Post; $post = new Post;
$this->app(); $this->app();
$this->assertNotContains('custom', $post->getDates()); $this->assertFalse($post->hasCast('custom'));
} }
/** /**
* @test * @test
*/ */
public function custom_date_attribute_can_be_set() public function custom_cast_attribute_can_be_set()
{ {
$this->extend( $this->extend(
(new Extend\Model(Post::class)) (new Extend\Model(Post::class))
->dateAttribute('custom') ->cast('custom', 'datetime')
); );
$this->app(); $this->app();
$post = new Post; $post = new Post;
$this->assertContains('custom', $post->getDates()); $this->assertTrue($post->hasCast('custom', 'datetime'));
} }
/** /**
* @test * @test
*/ */
public function custom_date_attribute_is_inherited_to_child_classes() public function custom_cast_attribute_is_inherited_to_child_classes()
{ {
$this->extend( $this->extend(
(new Extend\Model(Post::class)) (new Extend\Model(Post::class))
->dateAttribute('custom') ->cast('custom', 'boolean')
); );
$this->app(); $this->app();
$post = new CommentPost; $post = new CommentPost;
$this->assertContains('custom', $post->getDates()); $this->assertTrue($post->hasCast('custom', 'boolean'));
} }
/** /**
* @test * @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( $this->extend(
(new Extend\Model(Post::class)) (new Extend\Model(Post::class))
->dateAttribute('custom') ->cast('custom', 'integer')
); );
$this->app(); $this->app();
$discussion = new Discussion; $discussion = new Discussion;
$this->assertNotContains('custom', $discussion->getDates()); $this->assertFalse($discussion->hasCast('custom', 'integer'));
} }
} }

View File

@ -15,3 +15,27 @@ parameters:
- stubs/Illuminate/Contracts/Filesystem/Factory.stub - stubs/Illuminate/Contracts/Filesystem/Factory.stub
- stubs/Illuminate/Contracts/Filesystem/Cloud.stub - stubs/Illuminate/Contracts/Filesystem/Cloud.stub
- stubs/Illuminate/Contracts/Filesystem/Filesystem.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

View File

@ -0,0 +1,94 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PHPStan\Attributes;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Type;
class AttributeProperty implements PropertyReflection
{
/** @var ClassReflection */
private $classReflection;
/** @var Type */
private $type;
public function __construct(ClassReflection $classReflection, Type $type)
{
$this->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();
}
}

View File

@ -0,0 +1,87 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PHPStan\Attributes;
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;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\UnionType;
class ModelCastAttributeExtension implements PropertiesClassReflectionExtension
{
/** @var Resolver */
private $extendersResolver;
/** @var TypeStringResolver */
private $typeStringResolver;
public function __construct(Resolver $extendersResolver, TypeStringResolver $typeStringResolver)
{
$this->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);
}
}

View File

@ -0,0 +1,76 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
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
{
/** @var Resolver */
private $extendersResolver;
public function __construct(Resolver $extendersResolver)
{
$this->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(),
]));
}
}

View File

@ -0,0 +1,73 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PHPStan\Extender;
use PhpParser\Node\Expr;
use PhpParser\Node\Scalar;
class Extender
{
/** @var string */
public $qualifiedClassName;
/** @var Expr[] */
public $constructorArguments;
/** @var MethodCall[] */
public $methodCalls;
public function __construct(string $qualifiedClassName, array $constructorArguments = [], array $methodCalls = [])
{
$this->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;
}
}

View File

@ -0,0 +1,45 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PHPStan\Extender;
class FilesProvider
{
/** @var string[] */
private $cachedExtenderFiles;
/** @var string[] */
private $paths;
public function __construct(array $paths)
{
$this->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;
}
}

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PHPStan\Extender;
class MethodCall
{
/** @var string */
public $methodName;
/** @var array */
public $arguments;
public function __construct(string $methodName, array $arguments = [])
{
$this->methodName = $methodName;
$this->arguments = $arguments;
}
}

View File

@ -0,0 +1,138 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PHPStan\Extender;
use Flarum\PHPStan\Extender\MethodCall as ExtenderMethodCall;
use PhpParser\Node\Arg;
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;
class Resolver
{
/** @var Extender[] */
private $cachedExtenders = [];
/** @var FilesProvider */
private $extenderFilesProvider;
/** @var Parser */
private $parser;
public function __construct(FilesProvider $extenderFilesProvider, Parser $parser)
{
$this->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);
}
}

View File

@ -0,0 +1,86 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PHPStan\Relations;
use Flarum\PHPStan\Extender\MethodCall;
use Flarum\PHPStan\Extender\Resolver;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Reflection\PropertiesClassReflectionExtension;
use PHPStan\Reflection\PropertyReflection;
class ModelRelationsExtension implements MethodsClassReflectionExtension, PropertiesClassReflectionExtension
{
/** @var Resolver */
private $extendersResolver;
public function __construct(Resolver $extendersResolver)
{
$this->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);
}
}

View File

@ -0,0 +1,132 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PHPStan\Relations;
use Flarum\PHPStan\Extender\MethodCall;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
class RelationMethod implements MethodReflection
{
/** @var MethodCall */
private $methodCall;
/** @var ClassReflection */
private $classReflection;
public function __construct(MethodCall $methodCall, ClassReflection $classReflection)
{
$this->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();
}
}

View File

@ -0,0 +1,110 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PHPStan\Relations;
use Exception;
use Flarum\PHPStan\Extender\MethodCall;
use Illuminate\Database\Eloquent\Collection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
class RelationProperty implements PropertyReflection
{
/** @var MethodCall */
private $methodCall;
/** @var ClassReflection */
private $classReflection;
public function __construct(MethodCall $methodCall, ClassReflection $classReflection)
{
$this->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();
}
}