diff --git a/src/Reflection/Domain/Entities/Type/CollectionTypeReflection.php b/src/Reflection/Domain/Entities/Type/CollectionTypeReflection.php index 05320e9..de2f696 100644 --- a/src/Reflection/Domain/Entities/Type/CollectionTypeReflection.php +++ b/src/Reflection/Domain/Entities/Type/CollectionTypeReflection.php @@ -29,6 +29,11 @@ public static function recreate( return new static($collectionType, $keyType, $valueType); } + public function allowsNull(): bool + { + return false; + } + public function normalize(): array { return [ diff --git a/src/Reflection/Domain/Entities/Type/IntersectionTypeReflection.php b/src/Reflection/Domain/Entities/Type/IntersectionTypeReflection.php index 127c9fc..a840db7 100644 --- a/src/Reflection/Domain/Entities/Type/IntersectionTypeReflection.php +++ b/src/Reflection/Domain/Entities/Type/IntersectionTypeReflection.php @@ -30,6 +30,11 @@ public static function recreate( return new static($types); } + public function allowsNull(): bool + { + return false; + } + public function normalize(): array { return [ diff --git a/src/Reflection/Domain/Entities/Type/NamedTypeReflection.php b/src/Reflection/Domain/Entities/Type/NamedTypeReflection.php index fcdcd6e..0b59019 100644 --- a/src/Reflection/Domain/Entities/Type/NamedTypeReflection.php +++ b/src/Reflection/Domain/Entities/Type/NamedTypeReflection.php @@ -6,11 +6,11 @@ final class NamedTypeReflection extends TypeReflection { - public const IS_BUILT_IN = 0; - public const IS_CLASS = 1; - public const IS_INTERFACE = 2; - public const IS_ABSTRACT = 4; - public const IS_ENUM = 8; + public const IS_BUILT_IN = 1; + public const IS_CLASS = 2; + public const IS_INTERFACE = 4; + public const IS_ABSTRACT = 8; + public const IS_ENUM = 16; private function __construct( /** @var string|class-string */ @@ -94,6 +94,11 @@ public function isEnum(): bool return ($this->flags & self::IS_ENUM) == self::IS_ENUM; } + public function allowsNull(): bool + { + return 'null' === $this->name || 'mixed' === $this->name; + } + public function normalize(): array { return [ diff --git a/src/Reflection/Domain/Entities/Type/TypeReflection.php b/src/Reflection/Domain/Entities/Type/TypeReflection.php index 3563d36..3ab6995 100644 --- a/src/Reflection/Domain/Entities/Type/TypeReflection.php +++ b/src/Reflection/Domain/Entities/Type/TypeReflection.php @@ -8,4 +8,5 @@ abstract class TypeReflection implements Normalizable { + abstract public function allowsNull(): bool; } diff --git a/src/Reflection/Domain/Entities/Type/UnionTypeReflection.php b/src/Reflection/Domain/Entities/Type/UnionTypeReflection.php index d01bd54..418e322 100644 --- a/src/Reflection/Domain/Entities/Type/UnionTypeReflection.php +++ b/src/Reflection/Domain/Entities/Type/UnionTypeReflection.php @@ -30,6 +30,17 @@ public static function recreate( return new static($types); } + public function allowsNull(): bool + { + foreach ($this->types as $type) { + if ($type->allowsNull()) { + return true; + } + } + + return false; + } + public function normalize(): array { return [ diff --git a/src/Reflection/Domain/Factories/TypeReflectionFactory.php b/src/Reflection/Domain/Factories/TypeReflectionFactory.php index ab218ac..b3ed010 100644 --- a/src/Reflection/Domain/Factories/TypeReflectionFactory.php +++ b/src/Reflection/Domain/Factories/TypeReflectionFactory.php @@ -4,8 +4,11 @@ namespace PBaszak\UltraMapper\Reflection\Domain\Factories; +use PBaszak\UltraMapper\Reflection\Domain\Entities\Type\CollectionTypeReflection; +use PBaszak\UltraMapper\Reflection\Domain\Entities\Type\IntersectionTypeReflection; use PBaszak\UltraMapper\Reflection\Domain\Entities\Type\NamedTypeReflection; use PBaszak\UltraMapper\Reflection\Domain\Entities\Type\TypeReflection; +use PBaszak\UltraMapper\Reflection\Domain\Entities\Type\UnionTypeReflection; use phpDocumentor\Reflection\DocBlock\Tags\Param; use phpDocumentor\Reflection\DocBlock\Tags\Return_; use phpDocumentor\Reflection\DocBlock\Tags\Var_; @@ -45,16 +48,98 @@ public function create(?\ReflectionType $ref, ?PhpDocumentorReflectionType $docC // intersection // named - return NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN); + return $this->createTypeReflectionBasedOnReflectionType($ref); } - private function createNamedTypeReflection(?\ReflectionType $ref, ?PhpDocumentorReflectionType $docCommentRef): NamedTypeReflection + private function createTypeReflectionBasedOnReflectionType(?\ReflectionType $ref): TypeReflection { - if ($ref && !$ref instanceof \ReflectionNamedType) { - // todo + if ($ref instanceof \ReflectionNamedType) { + $name = $ref->getName(); + $flags = 0; + $isCollection = in_array($name, ['array', 'iterable']); + + if ($ref->isBuiltin()) { + $flags |= NamedTypeReflection::IS_BUILT_IN; + } + + if (class_exists($name, false) && !enum_exists($name, false)) { + $flags |= NamedTypeReflection::IS_CLASS; + $class = (new \ReflectionClass($name)); + + if ($class->isAbstract()) { + $flags |= NamedTypeReflection::IS_ABSTRACT; + } + + if ($class->implementsInterface(\Traversable::class) || $class->implementsInterface(\ArrayAccess::class)) { + $isCollection = true; + } + } + + if (interface_exists($name, false)) { + $flags |= NamedTypeReflection::IS_INTERFACE; + $interface = (new \ReflectionClass($name)); + + if ($interface->implementsInterface(\Traversable::class) || $interface->implementsInterface(\ArrayAccess::class)) { + $isCollection = true; + } + } + + if (enum_exists($name, false)) { + $flags |= NamedTypeReflection::IS_ENUM; + } + + $output = $isCollection + ? CollectionTypeReflection::create( + NamedTypeReflection::create($name, $flags), + \SplObjectStorage::class === $name || (@$class && $class->isSubclassOf(\SplObjectStorage::class)) + ? NamedTypeReflection::create('object', NamedTypeReflection::IS_BUILT_IN) + : UnionTypeReflection::create([ + NamedTypeReflection::create('int', NamedTypeReflection::IS_BUILT_IN), + NamedTypeReflection::create('string', NamedTypeReflection::IS_BUILT_IN), + ]), + NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN), + ) + : NamedTypeReflection::create($name, $flags); + + if ($ref->allowsNull()) { + if (!in_array($name, ['null', 'mixed'])) { + return UnionTypeReflection::create([ + NamedTypeReflection::create('null', NamedTypeReflection::IS_BUILT_IN), + $output, + ]); + } + + if ('null' === $name) { + return NamedTypeReflection::create('null', NamedTypeReflection::IS_BUILT_IN); + } + + if ('mixed' === $name) { + return NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN); + } + } + + return $output; } - throw new \LogicException('Not implemented'); + if ($ref instanceof \ReflectionUnionType) { + $types = array_map( + fn (\ReflectionType $type) => $this->createTypeReflectionBasedOnReflectionType($type), + $ref->getTypes() + ); + + return UnionTypeReflection::create($types); + } + + if ($ref instanceof \ReflectionIntersectionType) { + $types = array_map( + fn (\ReflectionType $type) => $this->createTypeReflectionBasedOnReflectionType($type), + $ref->getTypes() + ); + + return IntersectionTypeReflection::create($types); + } + + return NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN); } private function getDocBlockReflectionTypeFromVarTag(\ReflectionProperty $ref): ?PhpDocumentorReflectionType diff --git a/tests/Support/Assets/Enums/TestEnum.php b/tests/Support/Assets/Enums/TestEnum.php new file mode 100644 index 0000000..247eaf8 --- /dev/null +++ b/tests/Support/Assets/Enums/TestEnum.php @@ -0,0 +1,12 @@ +createForProperty((new \ReflectionProperty(get_class($obj), 'property'))); - + $reflection = $factory->createForProperty(new \ReflectionProperty(get_class($obj), 'property')); + $this->assertEquals('mixed', $reflection->name()); - $this->assertEquals(0, $reflection->flags()); + $this->assertEquals(NamedTypeReflection::IS_BUILT_IN, $reflection->flags()); $this->assertTrue($reflection->isBuiltIn()); } + + public static function createTypeReflectionBasedOnReflectionTypeDataProvider(): array + { + return [ + 'none' => [ + (new \ReflectionProperty(get_class(new class() { + public $property; + }), 'property'))->getType(), + NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN), + ], + 'mixed' => [ + (new \ReflectionProperty(get_class(new class() { + public mixed $property; + }), 'property'))->getType(), + NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN), + ], + 'null' => [ + (new \ReflectionProperty(get_class(new class() { + public null $property; + }), 'property'))->getType(), + NamedTypeReflection::create('null', NamedTypeReflection::IS_BUILT_IN), + ], + 'bool' => [ + (new \ReflectionProperty(get_class(new class() { + public bool $property; + }), 'property'))->getType(), + NamedTypeReflection::create('bool', NamedTypeReflection::IS_BUILT_IN), + ], + 'int' => [ + (new \ReflectionProperty(get_class(new class() { + public int $property; + }), 'property'))->getType(), + NamedTypeReflection::create('int', NamedTypeReflection::IS_BUILT_IN), + ], + 'float' => [ + (new \ReflectionProperty(get_class(new class() { + public float $property; + }), 'property'))->getType(), + NamedTypeReflection::create('float', NamedTypeReflection::IS_BUILT_IN), + ], + 'string' => [ + (new \ReflectionProperty(get_class(new class() { + public string $property; + }), 'property'))->getType(), + NamedTypeReflection::create('string', NamedTypeReflection::IS_BUILT_IN), + ], + 'array' => [ + (new \ReflectionProperty(get_class(new class() { + public array $property; + }), 'property'))->getType(), + CollectionTypeReflection::create( + NamedTypeReflection::create('array', NamedTypeReflection::IS_BUILT_IN), + UnionTypeReflection::create([ + NamedTypeReflection::create('int', NamedTypeReflection::IS_BUILT_IN), + NamedTypeReflection::create('string', NamedTypeReflection::IS_BUILT_IN), + ]), + NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN), + ), + ], + 'iterable' => [ + (new \ReflectionProperty(get_class(new class() { + public iterable $property; + }), 'property'))->getType(), + CollectionTypeReflection::create( + NamedTypeReflection::create('iterable', NamedTypeReflection::IS_BUILT_IN), + UnionTypeReflection::create([ + NamedTypeReflection::create('int', NamedTypeReflection::IS_BUILT_IN), + NamedTypeReflection::create('string', NamedTypeReflection::IS_BUILT_IN), + ]), + NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN), + ), + ], + 'object' => [ + (new \ReflectionProperty(get_class(new class() { + public object $property; + }), 'property'))->getType(), + NamedTypeReflection::create('object', NamedTypeReflection::IS_BUILT_IN), + ], + 'nullable' => [ + (new \ReflectionProperty(get_class(new class() { + public ?string $property; + }), 'property'))->getType(), + UnionTypeReflection::create([ + NamedTypeReflection::create('null', NamedTypeReflection::IS_BUILT_IN), + NamedTypeReflection::create('string', NamedTypeReflection::IS_BUILT_IN), + ]), + ], + 'union' => [ + (new \ReflectionProperty(get_class(new class() { + public int|string $property; + }), 'property'))->getType(), + UnionTypeReflection::create([ + NamedTypeReflection::create('string', NamedTypeReflection::IS_BUILT_IN), + NamedTypeReflection::create('int', NamedTypeReflection::IS_BUILT_IN), + ]), + ], + 'nullable union' => [ + (new \ReflectionProperty(get_class(new class() { + public int|string|null $property; + }), 'property'))->getType(), + UnionTypeReflection::create([ + NamedTypeReflection::create('string', NamedTypeReflection::IS_BUILT_IN), + NamedTypeReflection::create('int', NamedTypeReflection::IS_BUILT_IN), + NamedTypeReflection::create('null', NamedTypeReflection::IS_BUILT_IN), + ]), + ], + 'intersection' => [ + (new \ReflectionProperty(get_class(new class() { + public \DateTime&\DateTimeInterface $property; + }), 'property'))->getType(), + IntersectionTypeReflection::create([ + NamedTypeReflection::create('DateTime', NamedTypeReflection::IS_CLASS), + NamedTypeReflection::create('DateTimeInterface', NamedTypeReflection::IS_INTERFACE), + ]), + ], + 'nullable intersection' => [ + (new \ReflectionProperty(get_class(new class() { + public (\DateTime&\DateTimeInterface)|null $property; + }), 'property'))->getType(), + UnionTypeReflection::create([ + IntersectionTypeReflection::create([ + NamedTypeReflection::create('DateTime', NamedTypeReflection::IS_CLASS), + NamedTypeReflection::create('DateTimeInterface', NamedTypeReflection::IS_INTERFACE), + ]), + NamedTypeReflection::create('null', NamedTypeReflection::IS_BUILT_IN), + ]), + ], + 'class' => [ + (new \ReflectionProperty(get_class(new class() { + public \DateTime $property; + }), 'property'))->getType(), + NamedTypeReflection::create('DateTime', NamedTypeReflection::IS_CLASS), + ], + 'abstract class' => [ + (new \ReflectionProperty(get_class(new class() { + public \ReflectionType $property; + }), 'property'))->getType(), + NamedTypeReflection::create('ReflectionType', NamedTypeReflection::IS_CLASS | NamedTypeReflection::IS_ABSTRACT), + ], + 'array access' => [ + (new \ReflectionProperty(get_class(new class() { + public \ArrayAccess $property; + }), 'property'))->getType(), + CollectionTypeReflection::create( + NamedTypeReflection::create('ArrayAccess', NamedTypeReflection::IS_INTERFACE), + UnionTypeReflection::create([ + NamedTypeReflection::create('int', NamedTypeReflection::IS_BUILT_IN), + NamedTypeReflection::create('string', NamedTypeReflection::IS_BUILT_IN), + ]), + NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN), + ), + ], + 'traversable' => [ + (new \ReflectionProperty(get_class(new class() { + public \Traversable $property; + }), 'property'))->getType(), + CollectionTypeReflection::create( + NamedTypeReflection::create('Traversable', NamedTypeReflection::IS_INTERFACE), + UnionTypeReflection::create([ + NamedTypeReflection::create('int', NamedTypeReflection::IS_BUILT_IN), + NamedTypeReflection::create('string', NamedTypeReflection::IS_BUILT_IN), + ]), + NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN), + ), + ], + 'enum' => [ + (new \ReflectionProperty(get_class(new class() { + public TestEnum $property; + }), 'property'))->getType(), + NamedTypeReflection::create(TestEnum::class, NamedTypeReflection::IS_ENUM), + ], + 'interface' => [ + (new \ReflectionProperty(get_class(new class() { + public \DateTimeInterface $property; + }), 'property'))->getType(), + NamedTypeReflection::create('DateTimeInterface', NamedTypeReflection::IS_INTERFACE), + ], + 'spl object storage' => [ + (new \ReflectionProperty(get_class(new class() { + public \SplObjectStorage $property; + }), 'property'))->getType(), + CollectionTypeReflection::create( + NamedTypeReflection::create('SplObjectStorage', NamedTypeReflection::IS_CLASS), + NamedTypeReflection::create('object', NamedTypeReflection::IS_BUILT_IN), + NamedTypeReflection::create('mixed', NamedTypeReflection::IS_BUILT_IN) + ), + ], + ]; + } + + #[Test] + #[DataProvider('createTypeReflectionBasedOnReflectionTypeDataProvider')] + public function testCreateTypeReflectionBasedOnReflectionType(?\ReflectionType $input, TypeReflection $expected): void + { + $factory = new TypeReflectionFactory(); + $method = new \ReflectionMethod(TypeReflectionFactory::class, 'createTypeReflectionBasedOnReflectionType'); + + $result = $method->invokeArgs($factory, [$input]); + + $this->assertEquals($expected, $result); + } } diff --git a/tools/phpstan/fpm-baseline.neon b/tools/phpstan/fpm-baseline.neon index 55e0d5e..1bbbf9c 100644 --- a/tools/phpstan/fpm-baseline.neon +++ b/tools/phpstan/fpm-baseline.neon @@ -295,6 +295,16 @@ parameters: count: 1 path: ../../src/Reflection/Domain/Entities/Type/NamedTypeReflection.php + - + message: "#^Left side of && is always true\\.$#" + count: 1 + path: ../../src/Reflection/Domain/Factories/TypeReflectionFactory.php + + - + message: "#^Variable \\$class might not be defined\\.$#" + count: 1 + path: ../../src/Reflection/Domain/Factories/TypeReflectionFactory.php + - message: "#^Argument of an invalid type array\\\\|PBaszak\\\\UltraMapper\\\\Reflection\\\\Domain\\\\Entities\\\\ClassReflection supplied for foreach, only iterables are supported\\.$#" count: 1