Skip to content

Commit

Permalink
Rewrite ModelPropertyExtension with ModelCastHelper
Browse files Browse the repository at this point in the history
  • Loading branch information
erikgaal committed Sep 2, 2022
1 parent 30cd7c7 commit 60bab86
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 264 deletions.
3 changes: 3 additions & 0 deletions extension.neon
Expand Up @@ -408,6 +408,9 @@ services:
arguments:
schemaPaths: %squashedMigrationsPath%

-
class: NunoMaduro\Larastan\Properties\ModelCastHelper

-
class: NunoMaduro\Larastan\Rules\ModelProperties\ModelPropertiesRuleHelper

Expand Down
145 changes: 145 additions & 0 deletions src/Properties/ModelCastHelper.php
@@ -0,0 +1,145 @@
<?php

namespace NunoMaduro\Larastan\Properties;

use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

class ModelCastHelper
{
public function __construct(protected ReflectionProvider $reflectionProvider)
{
}

public function getReadableType(string $cast, Type $originalType): Type
{
$cast = Str::before($cast, ':');

$attributeType = match ($cast) {
'int', 'integer' => new IntegerType(),
'real', 'float', 'double' => new FloatType(),
'decimal' => new AccessoryNumericStringType(),
'string' => new StringType(),
'bool', 'boolean' => new BooleanType(),
'object' => new ObjectType('stdClass'),
'array', 'json' => new ArrayType(new MixedType(), new MixedType()),
'collection' => new ObjectType('Illuminate\Support\Collection'),
'date', 'datetime' => new ObjectType('Carbon\Carbon'),
'immutable_date', 'immutable_datetime' => new ObjectType('Carbon\CarbonImmutable'),
'timestamp' => new IntegerType(),
default => null,
};

if ($attributeType) {
return $attributeType;
}

if (! $this->reflectionProvider->hasClass($cast)) {
return new MixedType();
}

$classReflection = $this->reflectionProvider->getClass($cast);

if ($classReflection->isSubclassOf(Castable::class)) {
$methodReflection = $classReflection->getNativeMethod('castUsing');
$castUsingReturn = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();

if ($castUsingReturn instanceof ObjectType && $castReflection = $castUsingReturn->getClassReflection()) {
$classReflection = $castReflection;
}
}

if ($classReflection->isSubclassOf(CastsAttributes::class)) {
$methodReflection = $classReflection->getNativeMethod('get');

return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
}

if ($classReflection->isSubclassOf(CastsInboundAttributes::class)) {
return $originalType;
}

return new MixedType();
}

public function getWriteableType(string $cast, Type $originalType): Type
{
$attributeType = match ($cast) {
'int', 'integer' => new IntegerType(),
'real', 'float', 'double' => new FloatType(),
'decimal' => new AccessoryNumericStringType(),
'string' => new StringType(),
'bool', 'boolean' => TypeCombinator::union(new BooleanType(), new ConstantIntegerType(0), new ConstantIntegerType(1)),
'object' => new ObjectType('stdClass'),
'array', 'json' => new ArrayType(new MixedType(), new MixedType()),
'collection' => new ObjectType('Illuminate\Support\Collection'),
'date', 'datetime' => $this->getDateType(),
'immutable_date', 'immutable_datetime' => new ObjectType('Carbon\CarbonImmutable'),
'timestamp' => new IntegerType(),
default => null,
};

if ($attributeType) {
return $attributeType;
}

if (! $this->reflectionProvider->hasClass($cast)) {
return new MixedType();
}

$classReflection = $this->reflectionProvider->getClass($cast);

if ($classReflection->isSubclassOf(Castable::class)) {
$methodReflection = $classReflection->getNativeMethod('castUsing');
$castUsingReturn = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();

if ($castUsingReturn instanceof ObjectType && $castReflection = $castUsingReturn->getClassReflection()) {
$classReflection = $castReflection;
}
}

if (
$classReflection->isSubclassOf(CastsAttributes::class)
|| $classReflection->isSubclassOf(CastsInboundAttributes::class)
) {
$methodReflection = $classReflection->getNativeMethod('set');
$parameters = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getParameters();

$valueParameter = Arr::first($parameters, fn (ParameterReflection $parameterReflection) => $parameterReflection->getName() === 'value');

return $valueParameter->getType();
}

return new MixedType();
}

public function getDateType(): Type
{
$dateClass = class_exists(\Illuminate\Support\Facades\Date::class)
? \Illuminate\Support\Facades\Date::now()::class
: '\Illuminate\Support\Carbon';

if ($dateClass === '\Illuminate\Support\Carbon') {
return TypeCombinator::union(new ObjectType($dateClass), new ObjectType(\Carbon\Carbon::class));
}

return new ObjectType($dateClass);
}
}

0 comments on commit 60bab86

Please sign in to comment.