mirror of
https://github.com/flarum/framework.git
synced 2024-11-22 10:33:18 +08:00
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:
parent
2d2bf5c504
commit
5fe3cfd837
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
94
php-packages/phpstan/src/Attributes/AttributeProperty.php
Normal file
94
php-packages/phpstan/src/Attributes/AttributeProperty.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
73
php-packages/phpstan/src/Extender/Extender.php
Normal file
73
php-packages/phpstan/src/Extender/Extender.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
45
php-packages/phpstan/src/Extender/FilesProvider.php
Normal file
45
php-packages/phpstan/src/Extender/FilesProvider.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
24
php-packages/phpstan/src/Extender/MethodCall.php
Normal file
24
php-packages/phpstan/src/Extender/MethodCall.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
138
php-packages/phpstan/src/Extender/Resolver.php
Normal file
138
php-packages/phpstan/src/Extender/Resolver.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
132
php-packages/phpstan/src/Relations/RelationMethod.php
Normal file
132
php-packages/phpstan/src/Relations/RelationMethod.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
110
php-packages/phpstan/src/Relations/RelationProperty.php
Normal file
110
php-packages/phpstan/src/Relations/RelationProperty.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user