diff --git a/src/Annotation/DefaultValue.php b/src/Annotation/DefaultValue.php new file mode 100644 index 0000000..017d894 --- /dev/null +++ b/src/Annotation/DefaultValue.php @@ -0,0 +1,51 @@ + + * @copyright Copyright (c) 2021, Anatoly Nekhay + * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE + * @link https://github.com/sunrise-php/hydrator + */ + +declare(strict_types=1); + +namespace Sunrise\Hydrator\Annotation; + +use Attribute; + +/** + * @Annotation + * @Target({"PROPERTY"}) + * @NamedArgumentConstructor + * + * @Attributes({ + * @Attribute("value", type="mixed", required=true), + * }) + * + * @since 3.12.0 + */ +#[Attribute(Attribute::TARGET_PROPERTY)] +final class DefaultValue +{ + + /** + * The attribute value + * + * @var mixed + * + * @readonly + */ + public $value; + + /** + * Constructor of the class + * + * @param mixed $value + */ + public function __construct($value) + { + $this->value = $value; + } +} diff --git a/src/Annotation/Subtype.php b/src/Annotation/Subtype.php index 85af8a1..8d13f94 100644 --- a/src/Annotation/Subtype.php +++ b/src/Annotation/Subtype.php @@ -34,6 +34,13 @@ class Subtype { + /** + * @var mixed + * + * @internal + */ + public $holder = null; + /** * @var non-empty-string * diff --git a/src/Exception/InvalidObjectException.php b/src/Exception/InvalidObjectException.php index 52f99d1..140cecd 100644 --- a/src/Exception/InvalidObjectException.php +++ b/src/Exception/InvalidObjectException.php @@ -112,9 +112,11 @@ final public static function unsupportedParameterType(Type $type, ReflectionPara * * @since 3.2.0 */ - // phpcs:ignore Generic.Files.LineLength - final public static function unsupportedMethodParameterType(Type $type, ReflectionParameter $parameter, ReflectionMethod $method): self - { + final public static function unsupportedMethodParameterType( + Type $type, + ReflectionParameter $parameter, + ReflectionMethod $method + ): self { return new self(sprintf( 'The parameter {%s::%s($%s[%d])} is associated with an unsupported type {%s}.', $method->getDeclaringClass()->getName(), @@ -134,9 +136,11 @@ final public static function unsupportedMethodParameterType(Type $type, Reflecti * * @since 3.2.0 */ - // phpcs:ignore Generic.Files.LineLength - final public static function unsupportedFunctionParameterType(Type $type, ReflectionParameter $parameter, ReflectionFunctionAbstract $function): self - { + final public static function unsupportedFunctionParameterType( + Type $type, + ReflectionParameter $parameter, + ReflectionFunctionAbstract $function + ): self { return new self(sprintf( 'The parameter {%s($%s[%d])} is associated with an unsupported type {%s}.', $function->getName(), diff --git a/src/Hydrator.php b/src/Hydrator.php index f44914c..c5a6e79 100644 --- a/src/Hydrator.php +++ b/src/Hydrator.php @@ -19,6 +19,7 @@ use ReflectionClass; use Sunrise\Hydrator\Annotation\Alias; use Sunrise\Hydrator\Annotation\Context; +use Sunrise\Hydrator\Annotation\DefaultValue; use Sunrise\Hydrator\Annotation\Ignore; use Sunrise\Hydrator\AnnotationReader\BuiltinAnnotationReader; use Sunrise\Hydrator\AnnotationReader\DoctrineAnnotationReader; @@ -159,8 +160,10 @@ public function addTypeConverter(TypeConverterInterface ...$typeConverters): sel $this->typeConverters[] = $typeConverter; } - // phpcs:ignore Generic.Files.LineLength - usort($this->typeConverters, static fn(TypeConverterInterface $a, TypeConverterInterface $b): int => $b->getWeight() <=> $a->getWeight()); + usort($this->typeConverters, static fn( + TypeConverterInterface $a, + TypeConverterInterface $b + ): int => $b->getWeight() <=> $a->getWeight()); return $this; } @@ -196,9 +199,9 @@ public function castValue($value, Type $type, array $path = [], array $context = */ public function hydrate($object, array $data, array $path = [], array $context = []): object { - [$object, $class] = $this->instantObject($object); + [$object, $class] = self::instantObject($object); $properties = $class->getProperties(); - $constructorDefaultValues = $this->getClassConstructorDefaultValues($class); + $constructorDefaultValues = self::getConstructorDefaultValues($class); $violations = []; foreach ($properties as $property) { @@ -216,8 +219,8 @@ public function hydrate($object, array $data, array $path = [], array $context = continue; } - // phpcs:ignore Generic.Files.LineLength - $key = $this->annotationReader->getAnnotations(Alias::class, $property)->current()->value ?? $property->getName(); + $key = $this->annotationReader->getAnnotations(Alias::class, $property)->current()->value + ?? $property->getName(); if (array_key_exists($key, $data) === false) { if ($property->isInitialized($object)) { @@ -229,6 +232,12 @@ public function hydrate($object, array $data, array $path = [], array $context = continue; } + $defaultValue = $this->annotationReader->getAnnotations(DefaultValue::class, $property)->current(); + if ($defaultValue !== null) { + $property->setValue($object, $defaultValue->value); + continue; + } + $violations[] = InvalidValueException::mustBeProvided([...$path, $key]); continue; } @@ -243,7 +252,7 @@ public function hydrate($object, array $data, array $path = [], array $context = } } - if (!empty($violations)) { + if ($violations !== []) { throw new InvalidDataException('Invalid data', $violations); } @@ -264,8 +273,10 @@ public function hydrateWithJson($object, string $json, int $flags = 0, int $dept try { $data = json_decode($json, true, $depth, $flags | JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR); } catch (JsonException $e) { - // phpcs:ignore Generic.Files.LineLength - throw new InvalidDataException(sprintf('The JSON is invalid and couldn‘t be decoded due to: %s', $e->getMessage())); + throw new InvalidDataException(sprintf( + 'The JSON is invalid and couldn‘t be decoded due to: %s', + $e->getMessage(), + )); } if (!is_array($data)) { @@ -286,7 +297,7 @@ public function hydrateWithJson($object, string $json, int $flags = 0, int $dept * * @template T of object */ - private function instantObject($object): array + private static function instantObject($object): array { if (is_object($object)) { return [$object, new ReflectionClass($object)]; @@ -294,8 +305,10 @@ private function instantObject($object): array /** @psalm-suppress DocblockTypeContradiction */ if (!is_string($object)) { - // phpcs:ignore Generic.Files.LineLength - throw new TypeError(sprintf('Argument #1 ($object) must be of type object or string, %s given', gettype($object))); + throw new TypeError(sprintf( + 'Argument #1 ($object) must be of type object or string, %s given', + gettype($object), + )); } if (!class_exists($object)) { @@ -319,7 +332,7 @@ private function instantObject($object): array * * @template T of object */ - private function getClassConstructorDefaultValues(ReflectionClass $class): array + private static function getConstructorDefaultValues(ReflectionClass $class): array { $constructor = $class->getConstructor(); if ($constructor === null) { diff --git a/src/TypeConverter/ArrayAccessTypeConverter.php b/src/TypeConverter/ArrayAccessTypeConverter.php index cc9a73d..4bc9133 100644 --- a/src/TypeConverter/ArrayAccessTypeConverter.php +++ b/src/TypeConverter/ArrayAccessTypeConverter.php @@ -80,12 +80,12 @@ public function setHydrator(HydratorInterface $hydrator): void */ public function castValue($value, Type $type, array $path, array $context): Generator { - $containerName = $type->getName(); - if (!is_subclass_of($containerName, ArrayAccess::class)) { + $typeName = $type->getName(); + if (!is_subclass_of($typeName, ArrayAccess::class)) { return; } - $containerReflection = new ReflectionClass($containerName); + $containerReflection = new ReflectionClass($typeName); if (!$containerReflection->isInstantiable()) { throw InvalidObjectException::unsupportedType($type); } @@ -105,8 +105,8 @@ public function castValue($value, Type $type, array $path, array $context): Gene throw InvalidValueException::mustBeArray($path); } - // phpcs:ignore Generic.Files.LineLength - $subtype = $this->annotationReader->getAnnotations(Subtype::class, $type->getHolder())->current() ?? $this->getContainerSubtype($containerReflection); + $subtype = $this->annotationReader->getAnnotations(Subtype::class, $type->getHolder())->current() + ?? self::getContainerSubtype($containerReflection); if ($subtype === null) { $counter = 0; @@ -122,17 +122,19 @@ public function castValue($value, Type $type, array $path, array $context): Gene return yield $container; } - if (isset($subtype->limit) && count($value) > $subtype->limit) { + if ($subtype->limit !== null && count($value) > $subtype->limit) { throw InvalidValueException::arrayOverflow($path, $subtype->limit); } + $subtype->holder ??= $type->getHolder(); + $counter = 0; $violations = []; foreach ($value as $key => $element) { try { $container[$key] = $this->hydrator->castValue( $element, - new Type($type->getHolder(), $subtype->name, $subtype->allowsNull), + new Type($subtype->holder, $subtype->name, $subtype->allowsNull), [...$path, $key], $context, ); @@ -148,11 +150,11 @@ public function castValue($value, Type $type, array $path, array $context): Gene } } - if ($violations === []) { - return yield $container; + if ($violations !== []) { + throw new InvalidDataException('Invalid data', $violations); } - throw new InvalidDataException('Invalid data', $violations); + yield $container; } /** @@ -166,37 +168,46 @@ public function getWeight(): int /** * Gets a subtype from the given container's constructor * - * @param ReflectionClass $container + * @param ReflectionClass $class * * @return Subtype|null * * @codeCoverageIgnore */ - private function getContainerSubtype(ReflectionClass $container): ?Subtype + private static function getContainerSubtype(ReflectionClass $class): ?Subtype { - $constructor = $container->getConstructor(); + $constructor = $class->getConstructor(); if ($constructor === null) { return null; } - $parameters = $constructor->getParameters(); - if ($parameters === []) { + $constructorParameters = $constructor->getParameters(); + if ($constructorParameters === []) { return null; } - $parameter = end($parameters); - if ($parameter->isVariadic() === false) { + $lastConstructorParameter = end($constructorParameters); + if ($lastConstructorParameter->isVariadic() === false) { return null; } - $type = $parameter->getType(); - if ($type === null) { + $lastConstructorParameterType = $lastConstructorParameter->getType(); + if ($lastConstructorParameterType === null) { return null; } - /** @var non-empty-string $name */ - $name = ($type instanceof ReflectionNamedType) ? $type->getName() : (string) $type; + /** @var non-empty-string $lastConstructorParameterTypeName */ + $lastConstructorParameterTypeName = ($lastConstructorParameterType instanceof ReflectionNamedType) + ? $lastConstructorParameterType->getName() + : (string) $lastConstructorParameterType; + + $subtype = new Subtype( + $lastConstructorParameterTypeName, + $lastConstructorParameterType->allowsNull(), + ); + + $subtype->holder = $lastConstructorParameter; - return new Subtype($name, $type->allowsNull()); + return $subtype; } } diff --git a/src/TypeConverter/ArrayTypeConverter.php b/src/TypeConverter/ArrayTypeConverter.php index a2d47ba..4dd136c 100644 --- a/src/TypeConverter/ArrayTypeConverter.php +++ b/src/TypeConverter/ArrayTypeConverter.php @@ -96,7 +96,7 @@ public function castValue($value, Type $type, array $path, array $context): Gene return yield $value; } - if (isset($subtype->limit) && count($value) > $subtype->limit) { + if ($subtype->limit !== null && count($value) > $subtype->limit) { throw InvalidValueException::arrayOverflow($path, $subtype->limit); } @@ -116,11 +116,11 @@ public function castValue($value, Type $type, array $path, array $context): Gene } } - if ($violations === []) { - return yield $value; + if ($violations !== []) { + throw new InvalidDataException('Invalid data', $violations); } - throw new InvalidDataException('Invalid data', $violations); + yield $value; } /** diff --git a/src/TypeConverter/BackedEnumTypeConverter.php b/src/TypeConverter/BackedEnumTypeConverter.php index 9b0dd47..08932c1 100644 --- a/src/TypeConverter/BackedEnumTypeConverter.php +++ b/src/TypeConverter/BackedEnumTypeConverter.php @@ -16,7 +16,6 @@ use BackedEnum; use Generator; use ReflectionEnum; -use ReflectionNamedType; use Sunrise\Hydrator\Dictionary\BuiltinType; use Sunrise\Hydrator\Exception\InvalidValueException; use Sunrise\Hydrator\Type; @@ -54,11 +53,7 @@ public function castValue($value, Type $type, array $path, array $context): Gene return; } - /** @var ReflectionNamedType $enumType */ - $enumType = (new ReflectionEnum($enumName))->getBackingType(); - - /** @var BuiltinType::INT|BuiltinType::STRING $enumTypeName */ - $enumTypeName = $enumType->getName(); + $enumTypeName = (string) (new ReflectionEnum($enumName))->getBackingType(); if (is_string($value)) { $value = trim($value); diff --git a/src/TypeConverter/TimestampTypeConverter.php b/src/TypeConverter/TimestampTypeConverter.php index 260406c..8a291ca 100644 --- a/src/TypeConverter/TimestampTypeConverter.php +++ b/src/TypeConverter/TimestampTypeConverter.php @@ -83,8 +83,9 @@ public function castValue($value, Type $type, array $path, array $context): Gene throw InvalidObjectException::unsupportedType($type); } - // phpcs:ignore Generic.Files.LineLength - $format = $this->annotationReader->getAnnotations(Format::class, $type->getHolder())->current()->value ?? $context[ContextKey::TIMESTAMP_FORMAT] ?? self::DEFAULT_FORMAT; + $format = $this->annotationReader->getAnnotations(Format::class, $type->getHolder())->current()->value + ?? $context[ContextKey::TIMESTAMP_FORMAT] + ?? self::DEFAULT_FORMAT; if (is_string($value)) { $value = trim($value); diff --git a/tests/Fixture/BooleanArrayCollection.php b/tests/Fixture/BooleanArrayCollection.php new file mode 100644 index 0000000..38d05d1 --- /dev/null +++ b/tests/Fixture/BooleanArrayCollection.php @@ -0,0 +1,17 @@ +createHydrator()->hydrate($object, ['value' => [$data]]); } + /** + * @group array-access + */ + public function testHydrateBooleanArrayCollectionParameterWithValidData(): void + { + $this->phpRequired('8.0'); + + $object = new class { + public BooleanArrayCollection $value; + }; + + $this->assertInvalidValueExceptionCount(0); + $this->createHydrator()->hydrate($object, ['value' => [[true]]]); + $this->assertSame([[true]], (array) $object->value); + } + + /** + * @group array-access + */ + public function testHydrateBooleanArrayCollectionParameterWithInvalidData(): void + { + $this->phpRequired('8.0'); + + $object = new class { + public BooleanArrayCollection $value; + }; + + $this->assertInvalidValueExceptionCount(1); + $this->assertInvalidValueExceptionMessage(0, 'This value must be of type boolean.'); + $this->assertInvalidValueExceptionErrorCode(0, ErrorCode::MUST_BE_BOOLEAN); + $this->assertInvalidValueExceptionPropertyPath(0, 'value.0.0'); + $this->createHydrator()->hydrate($object, ['value' => [['foo']]]); + } + /** * @group association */ @@ -2610,6 +2646,19 @@ public function testAliasedProperty(): void $this->assertInvalidValueExceptionPropertyPath(0, 'value'); } + public function testDefaultValuedProperty(): void + { + $object = new class { + /** @DefaultValue("foo") */ + #[DefaultValue('foo')] + public string $value; + }; + + $this->assertInvalidValueExceptionCount(0); + $this->createHydrator()->hydrate($object, []); + $this->assertSame('foo', $object->value); + } + public function testUntypedProperty(): void { $object = new class {