Skip to content

Commit

Permalink
feat(phpstan): foundation for usage in extensions (#3666)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
SychO9 authored Jan 15, 2023
1 parent 2d2bf5c commit 5fe3cfd
Show file tree
Hide file tree
Showing 15 changed files with 934 additions and 28 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 6 additions & 8 deletions framework/core/src/Database/AbstractModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ abstract class AbstractModel extends Eloquent
/**
* @internal
*/
public static $dateAttributes = [];
public static $customCasts = [];

/**
* @internal
Expand Down Expand Up @@ -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;
}

/**
Expand Down
34 changes: 26 additions & 8 deletions framework/core/src/Extend/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -34,17 +35,25 @@ public function __construct(string $modelClass)
*
* @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;
}
Expand Down Expand Up @@ -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
)
);
}
}
22 changes: 11 additions & 11 deletions framework/core/tests/integration/extenders/ModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
->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'));
}
}

Expand Down
24 changes: 24 additions & 0 deletions php-packages/phpstan/extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
94 changes: 94 additions & 0 deletions php-packages/phpstan/src/Attributes/AttributeProperty.php
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 5fe3cfd

Please sign in to comment.