From 02dae2d63a0874fbbafb710deebf5755ea4d1c03 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Fri, 20 Sep 2024 15:53:50 +0200 Subject: [PATCH] Improve Serializer feature, nullable can be configured per field --- src/Serializer/CallbackCasting.php | 4 ++++ src/Serializer/CastToArray.php | 7 +++++++ src/Serializer/CastToBool.php | 12 ++++++++++-- src/Serializer/CastToDate.php | 9 ++++++++- src/Serializer/CastToEnum.php | 14 ++++++++++++-- src/Serializer/CastToFloat.php | 12 ++++++++++-- src/Serializer/CastToInt.php | 12 ++++++++++-- src/Serializer/CastToString.php | 12 ++++++++++-- 8 files changed, 71 insertions(+), 11 deletions(-) diff --git a/src/Serializer/CallbackCasting.php b/src/Serializer/CallbackCasting.php index 94705c8f..854a4c1a 100644 --- a/src/Serializer/CallbackCasting.php +++ b/src/Serializer/CallbackCasting.php @@ -92,6 +92,10 @@ public function setOptions(?string $type = null, mixed ...$options): void */ public function toVariable(mixed $value): mixed { + if ('' === $value && isset($this->options['allowEmptyStringAsNull']) && $this->options['allowEmptyStringAsNull']) { + $value = null; + } + try { return ($this->callback)($value, $this->isNullable, ...$this->options); } catch (Throwable $exception) { diff --git a/src/Serializer/CastToArray.php b/src/Serializer/CastToArray.php index 97fb36ec..20e4a372 100644 --- a/src/Serializer/CastToArray.php +++ b/src/Serializer/CastToArray.php @@ -44,6 +44,7 @@ final class CastToArray implements TypeCasting private int $depth = 512; private int $flags = 0; private ?array $default = null; + private bool $allowEmptyStringAsNull = false; /** * @throws MappingFailed @@ -71,6 +72,7 @@ public function setOptions( int $depth = 512, int $flags = 0, Type|string $type = Type::String, + ?bool $allowEmptyStringAsNull = null, ): void { if (!$shape instanceof ArrayShape) { $shape = ArrayShape::tryFrom($shape) ?? throw new MappingFailed('Unable to resolve the array shape; Verify your options arguments.'); @@ -94,10 +96,15 @@ public function setOptions( 1 !== strlen($this->enclosure) && $this->shape->equals(ArrayShape::Csv) => throw new MappingFailed('expects enclosure to be a single character; `'.$this->enclosure.'` given.'), default => $this->resolveFilterFlag($type), }; + $this->allowEmptyStringAsNull = $allowEmptyStringAsNull ?? false; } public function toVariable(mixed $value): ?array { + if ('' === $value && $this->allowEmptyStringAsNull) { + $value = null; + } + if (null === $value) { return match (true) { $this->isNullable, diff --git a/src/Serializer/CastToBool.php b/src/Serializer/CastToBool.php index 5f2a0c8b..524dd989 100644 --- a/src/Serializer/CastToBool.php +++ b/src/Serializer/CastToBool.php @@ -26,15 +26,19 @@ final class CastToBool implements TypeCasting private readonly bool $isNullable; private readonly Type $type; private ?bool $default = null; + private bool $allowEmptyStringAsNull = false; public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty) { [$this->type, $this->isNullable] = $this->init($reflectionProperty); } - public function setOptions(?bool $default = null): void - { + public function setOptions( + ?bool $default = null, + ?bool $allowEmptyStringAsNull = null, + ): void { $this->default = $default; + $this->allowEmptyStringAsNull = $allowEmptyStringAsNull ?? false; } /** @@ -42,6 +46,10 @@ public function setOptions(?bool $default = null): void */ public function toVariable(mixed $value): ?bool { + if ('' === $value && $this->allowEmptyStringAsNull) { + $value = null; + } + $returnValue = match (true) { is_bool($value) => $value, null !== $value => filter_var($value, Type::Bool->filterFlag()), diff --git a/src/Serializer/CastToDate.php b/src/Serializer/CastToDate.php index e45a913e..72a57576 100644 --- a/src/Serializer/CastToDate.php +++ b/src/Serializer/CastToDate.php @@ -38,6 +38,7 @@ final class CastToDate implements TypeCasting private readonly string $propertyName; private ?DateTimeZone $timezone = null; private ?string $format = null; + private bool $allowEmptyStringAsNull = false; /** * @throws MappingFailed @@ -58,7 +59,8 @@ public function setOptions( ?string $default = null, ?string $format = null, DateTimeZone|string|null $timezone = null, - ?string $className = null + ?string $className = null, + ?bool $allowEmptyStringAsNull = null, ): void { $this->class = match (true) { !interface_exists($this->class) && !Type::Mixed->equals($this->type) => $this->class, @@ -74,6 +76,7 @@ interface_exists($this->class) && null !== $className && class_exists($className } catch (Throwable $exception) { throw new MappingFailed('The `timezone` and/or `format` options used for `'.self::class.'` are invalud.', 0, $exception); } + $this->allowEmptyStringAsNull = $allowEmptyStringAsNull ?? false; } /** @@ -93,6 +96,10 @@ public function toVariable(mixed $value): DateTimeImmutable|DateTime|null */ private function cast(mixed $value): DateTimeImmutable|DateTime { + if ('' === $value && $this->allowEmptyStringAsNull) { + $value = null; + } + if ($value instanceof DateTimeInterface) { if ($value instanceof $this->class) { return $value; diff --git a/src/Serializer/CastToEnum.php b/src/Serializer/CastToEnum.php index 21e07522..f4192fa2 100644 --- a/src/Serializer/CastToEnum.php +++ b/src/Serializer/CastToEnum.php @@ -31,6 +31,7 @@ class CastToEnum implements TypeCasting private readonly string $propertyName; /** @var class-string */ private string $class; + private bool $allowEmptyStringAsNull = false; /** * @throws MappingFailed @@ -46,8 +47,11 @@ public function __construct(ReflectionProperty|ReflectionParameter $reflectionPr * * @throws MappingFailed */ - public function setOptions(?string $default = null, ?string $className = null): void - { + public function setOptions( + ?string $default = null, + ?string $className = null, + ?bool $allowEmptyStringAsNull = null, + ): void { if (Type::Mixed->equals($this->type) || in_array($this->class, [BackedEnum::class , UnitEnum::class], true)) { if (null === $className || !enum_exists($className)) { throw new MappingFailed('`'.$this->propertyName.'` type is `'.($this->class ?? 'mixed').'` but the specified class via the `$className` argument is invalid or could not be found.'); @@ -61,6 +65,8 @@ public function setOptions(?string $default = null, ?string $className = null): } catch (TypeCastingFailed $exception) { throw new MappingFailed(message:'The `default` option is invalid.', previous: $exception); } + + $this->allowEmptyStringAsNull = $allowEmptyStringAsNull ?? false; } /** @@ -68,6 +74,10 @@ public function setOptions(?string $default = null, ?string $className = null): */ public function toVariable(mixed $value): BackedEnum|UnitEnum|null { + if ('' === $value && $this->allowEmptyStringAsNull) { + $value = null; + } + return match (true) { null !== $value => $this->cast($value), $this->isNullable => $this->default, diff --git a/src/Serializer/CastToFloat.php b/src/Serializer/CastToFloat.php index f8a8f778..78e494c6 100644 --- a/src/Serializer/CastToFloat.php +++ b/src/Serializer/CastToFloat.php @@ -25,15 +25,19 @@ final class CastToFloat implements TypeCasting { private readonly bool $isNullable; private ?float $default = null; + private bool $allowEmptyStringAsNull = false; public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty) { $this->isNullable = $this->init($reflectionProperty); } - public function setOptions(int|float|null $default = null): void - { + public function setOptions( + int|float|null $default = null, + ?bool $allowEmptyStringAsNull = null, + ): void { $this->default = $default; + $this->allowEmptyStringAsNull = $allowEmptyStringAsNull ?? false; } /** @@ -41,6 +45,10 @@ public function setOptions(int|float|null $default = null): void */ public function toVariable(mixed $value): ?float { + if ('' === $value && $this->allowEmptyStringAsNull) { + $value = null; + } + if (null === $value) { return match ($this->isNullable) { true => $this->default, diff --git a/src/Serializer/CastToInt.php b/src/Serializer/CastToInt.php index 7d673968..33301929 100644 --- a/src/Serializer/CastToInt.php +++ b/src/Serializer/CastToInt.php @@ -25,15 +25,19 @@ final class CastToInt implements TypeCasting { private readonly bool $isNullable; private ?int $default = null; + private bool $allowEmptyStringAsNull = false; public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty) { $this->isNullable = $this->init($reflectionProperty); } - public function setOptions(?int $default = null): void - { + public function setOptions( + ?int $default = null, + ?bool $allowEmptyStringAsNull = null, + ): void { $this->default = $default; + $this->allowEmptyStringAsNull = $allowEmptyStringAsNull ?? false; } /** @@ -41,6 +45,10 @@ public function setOptions(?int $default = null): void */ public function toVariable(mixed $value): ?int { + if ('' === $value && $this->allowEmptyStringAsNull) { + $value = null; + } + if (null === $value) { return match ($this->isNullable) { true => $this->default, diff --git a/src/Serializer/CastToString.php b/src/Serializer/CastToString.php index 1a24c8da..df3a3cf5 100644 --- a/src/Serializer/CastToString.php +++ b/src/Serializer/CastToString.php @@ -24,15 +24,19 @@ final class CastToString implements TypeCasting private readonly bool $isNullable; private readonly Type $type; private ?string $default = null; + private bool $allowEmptyStringAsNull = false; public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty) { [$this->type, $this->isNullable] = $this->init($reflectionProperty); } - public function setOptions(?string $default = null): void - { + public function setOptions( + ?string $default = null, + ?bool $allowEmptyStringAsNull = null, + ): void { $this->default = $default; + $this->allowEmptyStringAsNull = $allowEmptyStringAsNull ?? false; } /** @@ -40,6 +44,10 @@ public function setOptions(?string $default = null): void */ public function toVariable(mixed $value): ?string { + if ('' === $value && $this->allowEmptyStringAsNull) { + $value = null; + } + $returnedValue = match (true) { is_string($value) => $value, $this->isNullable => $this->default,