diff --git a/composer.json b/composer.json index 3c12ac8..98984bd 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "yzen.dev/plain-to-class", - "version": "3.0.3", + "version": "3.0.4", "description": "Class-transformer to transform your dataset into a structured object", "minimum-stability": "dev", "prefer-stable": true, diff --git a/composer.lock b/composer.lock index bc4682e..d23fbb9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "38393cf49708cbd2c627803180a174c8", + "content-hash": "fe8f42c900ac36a6eb10fd83777d6411", "packages": [], "packages-dev": [ { diff --git a/src/ArgumentsRepository.php b/src/ArgumentsRepository.php index 3da6161..efbcf2d 100644 --- a/src/ArgumentsRepository.php +++ b/src/ArgumentsRepository.php @@ -26,7 +26,7 @@ final class ArgumentsRepository /** * - * @param iterable|array ...$args + * @param iterable|object ...$args */ public function __construct(...$args) { diff --git a/src/Attributes/EmptyToNull.php b/src/Attributes/EmptyToNull.php new file mode 100644 index 0000000..7d64b79 --- /dev/null +++ b/src/Attributes/EmptyToNull.php @@ -0,0 +1,16 @@ +type, $property->hasSetMutator(), $property->notTransform(), + $property->convertEmptyToNull(), $property->getDocComment(), $args, $this->getAliases($args), diff --git a/src/ClassRepository.php b/src/ClassRepository.php index 6855b64..5ea9c06 100644 --- a/src/ClassRepository.php +++ b/src/ClassRepository.php @@ -3,8 +3,6 @@ namespace ClassTransformer; use ClassTransformer\Contracts\ReflectionProperty; -use ClassTransformer\Exceptions\ClassNotFoundException; -use ClassTransformer\Reflection\CacheReflectionProperty; use ClassTransformer\Contracts\ReflectionClassRepository; /** @@ -14,22 +12,21 @@ */ final class ClassRepository { - /** @var class-string $class */ + /** @var string $class */ private string $class; /** @var ReflectionClassRepository $class */ private ReflectionClassRepository $classRepository; /** - * @var array + * @var array */ private static array $propertiesTypesCache = []; /** - * @param class-string $class - * - * @throws ClassNotFoundException + * @param string $class + * @param ReflectionClassRepository $classRepository */ public function __construct( string $class, diff --git a/src/Contracts/ReflectionClassRepository.php b/src/Contracts/ReflectionClassRepository.php index 8ec6f8b..919f2fb 100644 --- a/src/Contracts/ReflectionClassRepository.php +++ b/src/Contracts/ReflectionClassRepository.php @@ -8,7 +8,7 @@ interface ReflectionClassRepository { /** - * @return array + * @return ReflectionProperty[] */ public function getProperties(): array; diff --git a/src/Contracts/ReflectionProperty.php b/src/Contracts/ReflectionProperty.php index 660e4ea..249f5f7 100644 --- a/src/Contracts/ReflectionProperty.php +++ b/src/Contracts/ReflectionProperty.php @@ -19,14 +19,14 @@ abstract class ReflectionProperty public PropertyType $type; /** - * @param string $name + * @param class-string $name * * @return mixed */ abstract public function getAttribute(string $name): mixed; /** - * @param string $name + * @param class-string $name * * @return null|array */ @@ -47,6 +47,11 @@ abstract public function hasSetMutator(): bool; */ abstract public function notTransform(): bool; + /** + * @return bool + */ + abstract public function convertEmptyToNull(): bool; + /** * @return array */ diff --git a/src/Reflection/CacheReflectionClass.php b/src/Reflection/CacheReflectionClass.php index fbbc976..e3d3b27 100644 --- a/src/Reflection/CacheReflectionClass.php +++ b/src/Reflection/CacheReflectionClass.php @@ -17,14 +17,15 @@ */ final class CacheReflectionClass implements ReflectionClassRepository { - /** @var class-string $class */ + /** @var string $class */ private string $class; /** @var array CacheReflectionProperty[] */ private array $properties; /** - * @param class-string $class + * @param string $class + * @param CacheReflectionProperty[] $properties */ public function __construct(string $class, array $properties) { diff --git a/src/Reflection/CacheReflectionProperty.php b/src/Reflection/CacheReflectionProperty.php index dee7514..c2ba740 100644 --- a/src/Reflection/CacheReflectionProperty.php +++ b/src/Reflection/CacheReflectionProperty.php @@ -14,6 +14,15 @@ final class CacheReflectionProperty extends \ClassTransformer\Contracts\ReflectionProperty { /** + * @param class-string $class + * @param class-string|string $name + * @param PropertyType $type + * @param bool $hasSetMutator + * @param bool $notTransform + * @param bool $convertEmptyToNull + * @param string $docComment + * @param array $attributes + * @param array $aliases */ public function __construct( public string $class, @@ -21,6 +30,7 @@ public function __construct( public PropertyType $type, public bool $hasSetMutator, public bool $notTransform, + public bool $convertEmptyToNull, public string $docComment, public array $attributes, public array $aliases, @@ -42,6 +52,14 @@ public function notTransform(): bool { return $this->notTransform; } + + /** + * @return bool + */ + public function convertEmptyToNull(): bool + { + return $this->convertEmptyToNull; + } /** * @param string $name diff --git a/src/Reflection/RuntimeReflectionProperty.php b/src/Reflection/RuntimeReflectionProperty.php index 9738174..e5f1709 100644 --- a/src/Reflection/RuntimeReflectionProperty.php +++ b/src/Reflection/RuntimeReflectionProperty.php @@ -4,6 +4,7 @@ namespace ClassTransformer\Reflection; +use ClassTransformer\Attributes\EmptyToNull; use ReflectionProperty; use ReflectionAttribute; use ClassTransformer\TransformUtils; @@ -58,7 +59,15 @@ public function notTransform(): bool } /** - * @param string $name + * @return bool + */ + public function convertEmptyToNull(): bool + { + return $this->getAttribute(EmptyToNull::class) !== null; + } + + /** + * @param class-string $name * * @template T * @return null|ReflectionAttribute @@ -77,11 +86,11 @@ public function getAttribute(string $name): ?ReflectionAttribute } /** - * @param string|null $name + * @param class-string $name * * @return null|array */ - public function getAttributeArguments(?string $name = null): ?array + public function getAttributeArguments(string $name): ?array { return $this->getAttribute($name)?->getArguments(); } diff --git a/src/Reflection/Types/ArrayType.php b/src/Reflection/Types/ArrayType.php index 4b1c7b2..a2bfe15 100644 --- a/src/Reflection/Types/ArrayType.php +++ b/src/Reflection/Types/ArrayType.php @@ -11,6 +11,8 @@ */ class ArrayType extends PropertyType { + /** @var string|class-string */ public string $itemsType; public bool $isScalarItems; + } diff --git a/src/Reflection/Types/EnumType.php b/src/Reflection/Types/EnumType.php index d8087e3..4c01eb3 100644 --- a/src/Reflection/Types/EnumType.php +++ b/src/Reflection/Types/EnumType.php @@ -5,6 +5,7 @@ /** * Class EnumType * + * @psalm-api * @author yzen.dev */ class EnumType extends PropertyType diff --git a/src/Reflection/Types/PropertyType.php b/src/Reflection/Types/PropertyType.php index e608e0b..05568ee 100644 --- a/src/Reflection/Types/PropertyType.php +++ b/src/Reflection/Types/PropertyType.php @@ -7,12 +7,13 @@ /** * Class PropertyType * + * @psalm-api * @author yzen.dev */ class PropertyType { /** - * @param string $name Name of type + * @param string|class-string $name Name of type * @param bool $isScalar * @param bool $isNullable */ diff --git a/src/Reflection/Types/PropertyTypeFactory.php b/src/Reflection/Types/PropertyTypeFactory.php index 245a112..2edd61c 100644 --- a/src/Reflection/Types/PropertyTypeFactory.php +++ b/src/Reflection/Types/PropertyTypeFactory.php @@ -32,7 +32,7 @@ public static function create(RuntimeReflectionProperty $property) $isNullable = true; if ($reflectionType instanceof ReflectionType) { - $type = $reflectionType; + $type = (string)$reflectionType; $isNullable = $reflectionType->allowsNull(); } if ($reflectionType instanceof ReflectionNamedType) { @@ -41,6 +41,14 @@ public static function create(RuntimeReflectionProperty $property) $isNullable = $reflectionType->allowsNull(); } + if ($property->notTransform()) { + return new ScalarType( + $type, + $isScalar, + $isNullable + ); + } + if ($type === TypeEnums::TYPE_ARRAY) { $arrayTypeAttr = $property->getAttributeArguments(ConvertArray::class); @@ -50,18 +58,19 @@ public static function create(RuntimeReflectionProperty $property) $arrayType = TransformUtils::getClassFromPhpDoc($property->getDocComment()); } $arrayType ??= TypeEnums::TYPE_MIXED; - $type = new ArrayType( + + $typeInstance = new ArrayType( $type, $isScalar, $isNullable ); - $type->itemsType = $arrayType ?? TypeEnums::TYPE_MIXED; - $type->isScalarItems = in_array($arrayType, [TypeEnums::TYPE_INTEGER, TypeEnums::TYPE_FLOAT, TypeEnums::TYPE_STRING, TypeEnums::TYPE_BOOLEAN, TypeEnums::TYPE_MIXED]); + $typeInstance->itemsType = $arrayType ?? TypeEnums::TYPE_MIXED; + $typeInstance->isScalarItems = in_array($arrayType, [TypeEnums::TYPE_INTEGER, TypeEnums::TYPE_FLOAT, TypeEnums::TYPE_STRING, TypeEnums::TYPE_BOOLEAN, TypeEnums::TYPE_MIXED]); - return $type; + return $typeInstance; } - if ($isScalar || $property->notTransform()) { + if ($isScalar) { return new ScalarType( $type, $isScalar, @@ -80,7 +89,6 @@ public static function create(RuntimeReflectionProperty $property) return new TransformableType( $type, $isScalar, - $isNullable ); } } diff --git a/src/Reflection/Types/ScalarType.php b/src/Reflection/Types/ScalarType.php index 98901b4..ea14aee 100644 --- a/src/Reflection/Types/ScalarType.php +++ b/src/Reflection/Types/ScalarType.php @@ -7,6 +7,7 @@ /** * Class ScalarType * + * @psalm-api * @author yzen.dev */ class ScalarType extends PropertyType diff --git a/src/Reflection/Types/TransformableType.php b/src/Reflection/Types/TransformableType.php index 244d3ca..eac1427 100644 --- a/src/Reflection/Types/TransformableType.php +++ b/src/Reflection/Types/TransformableType.php @@ -11,4 +11,14 @@ */ class TransformableType extends PropertyType { + /** + * @param class-string $name Name of type + * @param bool $isNullable + */ + public function __construct( + public string $name, + public bool $isNullable + ) { + parent::__construct($this->name, false, $isNullable); + } } diff --git a/src/ValueCasting.php b/src/ValueCasting.php index bebe006..5676c7e 100644 --- a/src/ValueCasting.php +++ b/src/ValueCasting.php @@ -10,7 +10,7 @@ use ClassTransformer\Reflection\Types\ArrayType; use ClassTransformer\Contracts\ReflectionProperty; use ClassTransformer\Exceptions\ClassNotFoundException; -use ClassTransformer\Reflection\Types\TransformableType; +use ClassTransformer\Exceptions\InvalidArgumentException; use function array_map; use function is_array; @@ -44,6 +44,7 @@ public function __construct(ReflectionProperty $property, HydratorConfig $config * * @return mixed * @throws ClassNotFoundException|RuntimeException + * @throws InvalidArgumentException */ public function castAttribute(mixed $value): mixed { @@ -51,7 +52,15 @@ public function castAttribute(mixed $value): mixed return null; } - if (($this->property->type->isScalar && !$this->property->type instanceof ArrayType) || $this->property->notTransform()) { + if (($value === '' || $value === []) && $this->property->convertEmptyToNull()) { + return null; + } + + if ($this->property->notTransform() || $this->property->type->name === TypeEnums::TYPE_MIXED) { + return $value; + } + + if (in_array($this->property->type->name, [TypeEnums::TYPE_STRING, TypeEnums::TYPE_INTEGER, TypeEnums::TYPE_FLOAT, TypeEnums::TYPE_BOOLEAN])) { return $this->castScalar($this->property->type->name, $value); } @@ -73,15 +82,23 @@ public function castAttribute(mixed $value): mixed * @param mixed $value * * @return mixed + * @throws InvalidArgumentException */ private function castScalar(string $type, mixed $value): mixed { + + $providedType = gettype($value); + + if ($this->property->type->name !== TypeEnums::TYPE_MIXED && !in_array($providedType, ['integer', 'string', 'boolean', 'double'])) { + throw new InvalidArgumentException('Parameter `' . $this->property->name . '` expected type `' . $type . '`, `' . $providedType . '` provided'); + } + return match ($type) { TypeEnums::TYPE_STRING => (string)$value, TypeEnums::TYPE_INTEGER => (int)$value, TypeEnums::TYPE_FLOAT => (float)$value, TypeEnums::TYPE_BOOLEAN => (bool)$value, - default => $value + TypeEnums::TYPE_MIXED => $value, }; } @@ -90,6 +107,7 @@ private function castScalar(string $type, mixed $value): mixed * * @return array|mixed * @throws ClassNotFoundException + * @throws InvalidArgumentException */ private function castArray($value): mixed { @@ -100,6 +118,10 @@ private function castArray($value): mixed return array_map(fn($el) => (new Hydrator($this->config))->create($this->property->type->itemsType, $el), $value); } + if ($this->property->type->itemsType === TypeEnums::TYPE_MIXED) { + return $value; + } + return array_map(fn($item) => $this->castScalar($this->property->type->itemsType, $item), $value); } diff --git a/tests/Units/DTO/TypesDto.php b/tests/Units/DTO/TypesDto.php index 4819397..96caeff 100644 --- a/tests/Units/DTO/TypesDto.php +++ b/tests/Units/DTO/TypesDto.php @@ -4,6 +4,7 @@ namespace Tests\Units\DTO; +use ClassTransformer\Attributes\EmptyToNull; use ClassTransformer\Attributes\WritingStyle; class TypesDto @@ -11,6 +12,8 @@ class TypesDto public ?int $nullableInt; public ?string $nullableString; + #[EmptyToNull] + public ?string $emptyString; public ?float $nullableFloat; public ?bool $nullableBool; diff --git a/tests/Units/ValueCastingTest.php b/tests/Units/ValueCastingTest.php index 320edd0..56eed06 100644 --- a/tests/Units/ValueCastingTest.php +++ b/tests/Units/ValueCastingTest.php @@ -2,6 +2,7 @@ namespace Tests\Units; +use ClassTransformer\Exceptions\InvalidArgumentException; use ClassTransformer\ValueCasting; use PHPUnit\Framework\TestCase; use Tests\Units\DTO\ExtendedDto; @@ -11,6 +12,15 @@ class ValueCastingTest extends TestCase { + public function testCreateNotValidProperty(): void + { + $this->expectException(InvalidArgumentException::class); + $caster = new ValueCasting( + new RuntimeReflectionProperty(new \ReflectionProperty(ExtendedDto::class, 'id')) + ); + $caster->castAttribute([1,2]); + } + public function testCreateProperty(): void { $caster = new ValueCasting( @@ -56,6 +66,10 @@ public function testCreateProperty(): void $caster = new ValueCasting(new RuntimeReflectionProperty(new \ReflectionProperty(TypesDto::class, 'nullableString'))); $value = $caster->castAttribute(null); $this->assertNull($value); + + $caster = new ValueCasting(new RuntimeReflectionProperty(new \ReflectionProperty(TypesDto::class, 'emptyString'))); + $value = $caster->castAttribute(''); + $this->assertNull($value); $caster = new ValueCasting(new RuntimeReflectionProperty(new \ReflectionProperty(TypesDto::class, 'nullableFloat'))); $value = $caster->castAttribute(null); @@ -64,6 +78,7 @@ public function testCreateProperty(): void $caster = new ValueCasting(new RuntimeReflectionProperty(new \ReflectionProperty(TypesDto::class, 'nullableBool'))); $value = $caster->castAttribute(null); $this->assertNull($value); + } public function testCreateArrayProperty(): void