diff --git a/CHANGELOG.md b/CHANGELOG.md index d4bbf52..18618bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ # Yii Hydrator Change Log -## 1.0.1 under development +## 1.1.0 under development -- no changes in this release. +- New #74: Add option `castEmptyStringToNull` to `PhpNativeTypeCaster` (@vjik) ## 1.0.0 January 29, 2024 -- Initial release. \ No newline at end of file +- Initial release. diff --git a/src/TypeCaster/PhpNativeTypeCaster.php b/src/TypeCaster/PhpNativeTypeCaster.php index 2d65a24..d7127c3 100644 --- a/src/TypeCaster/PhpNativeTypeCaster.php +++ b/src/TypeCaster/PhpNativeTypeCaster.php @@ -23,6 +23,11 @@ */ final class PhpNativeTypeCaster implements TypeCasterInterface { + public function __construct( + public bool $castEmptyStringToNull = false, + ) { + } + public function cast(mixed $value, TypeCastContext $context): Result { $type = $context->getReflectionType(); @@ -49,8 +54,12 @@ public function cast(mixed $value, TypeCastContext $context): Result * - when pass `"42"` to `int|string` type, `string` will be used. */ foreach ($types as $t) { - if ($value === null && $t->allowsNull()) { - return Result::success(null); + if ($t->allowsNull()) { + if ($value === null + || ($this->castEmptyStringToNull && $value === '') + ) { + return Result::success(null); + } } if (!$t->isBuiltin()) { continue; @@ -108,8 +117,7 @@ public function cast(mixed $value, TypeCastContext $context): Result return Result::success((int) $value); } if ($value instanceof Stringable || is_string($value)) { - $value = NumericHelper::normalize($value); - return Result::success($t->allowsNull() && $value === '' ? null : (int) $value); + return Result::success((int) NumericHelper::normalize($value)); } break; @@ -118,15 +126,11 @@ public function cast(mixed $value, TypeCastContext $context): Result return Result::success((float) $value); } if ($value instanceof Stringable || is_string($value)) { - $value = NumericHelper::normalize($value); - return Result::success($t->allowsNull() && $value === '' ? null : (float) $value); + return Result::success((float) NumericHelper::normalize($value)); } break; case 'bool': - if ($t->allowsNull() && $value === '') { - return Result::success(null); - } if (is_scalar($value) || $value === null || is_array($value) || is_object($value)) { return Result::success((bool) $value); } diff --git a/tests/TypeCaster/PhpNativeTypeCasterTest.php b/tests/TypeCaster/PhpNativeTypeCasterTest.php index 9e282aa..33e7689 100644 --- a/tests/TypeCaster/PhpNativeTypeCasterTest.php +++ b/tests/TypeCaster/PhpNativeTypeCasterTest.php @@ -26,7 +26,7 @@ public function dataBase(): array static fn(float $a) => null, ], 'empty string to int|null' => [ - Result::success(null), + Result::success(0), '', static fn(?int $a) => null, ], @@ -36,15 +36,20 @@ public function dataBase(): array static fn(?string $a) => null, ], 'empty string to float|null' => [ - Result::success(null), + Result::success(0.0), '', static fn(?float $a) => null, ], 'empty string to bool|null' => [ - Result::success(null), + Result::success(false), '', static fn(?bool $a) => null, ], + 'empty string to array|null' => [ + Result::fail(), + '', + static fn(?array $a) => null, + ], ]; } @@ -61,4 +66,50 @@ public function testBase(Result $expected, mixed $value, Closure $closure): void $this->assertSame($expected->isResolved(), $result->isResolved()); $this->assertSame($expected->getValue(), $result->getValue()); } + + public function dataWithCastEmptyStringToNull(): array + { + + return [ + 'empty string to int|null' => [ + Result::success(null), + '', + static fn(?int $a) => null, + ], + 'empty string to string|null' => [ + Result::success(null), + '', + static fn(?string $a) => null, + ], + 'empty string to float|null' => [ + Result::success(null), + '', + static fn(?float $a) => null, + ], + 'empty string to bool|null' => [ + Result::success(null), + '', + static fn(?bool $a) => null, + ], + 'empty string to array|null' => [ + Result::success(null), + '', + static fn(?array $a) => null, + ], + ]; + } + + /** + * @dataProvider dataWithCastEmptyStringToNull + */ + public function testWithCastEmptyStringToNull(Result $expected, mixed $value, Closure $closure): void + { + $typeCaster = new PhpNativeTypeCaster(castEmptyStringToNull: true); + $context = TestHelper::createTypeCastContext($closure); + + $result = $typeCaster->cast($value, $context); + + $this->assertSame($expected->isResolved(), $result->isResolved()); + $this->assertSame($expected->getValue(), $result->getValue()); + } }