From 750a7f46402ff24b76ab4221ecaa17816ead8350 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 9 Feb 2024 10:44:50 +0300 Subject: [PATCH] Add `NullTypeCaster` (#74) Co-authored-by: Alexander Makarov --- CHANGELOG.md | 6 +- docs/guide/en/typecasting.md | 1 + src/TypeCaster/NullTypeCaster.php | 62 ++++++++++ tests/TypeCaster/NullTypeCasterTest.php | 158 ++++++++++++++++++++++++ 4 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 src/TypeCaster/NullTypeCaster.php create mode 100644 tests/TypeCaster/NullTypeCasterTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d4bbf52..408f2f5 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 `NullTypeCaster` (@vjik) ## 1.0.0 January 29, 2024 -- Initial release. \ No newline at end of file +- Initial release. diff --git a/docs/guide/en/typecasting.md b/docs/guide/en/typecasting.md index c38c8e7..4f3c4af 100644 --- a/docs/guide/en/typecasting.md +++ b/docs/guide/en/typecasting.md @@ -38,6 +38,7 @@ Out of the box, the following type-casters are available: - `CompositeTypeCaster` allows combining multiple type-casters - `PhpNativeTypeCaster` casts based on PHP types defined in the class - `HydratorTypeCaster` casts arrays to objects +- `NullTypeCaster` configurable type caster for casting `null`, empty string and empty array to `null` - `NoTypeCaster` does not cast anything ## Your own type-casting diff --git a/src/TypeCaster/NullTypeCaster.php b/src/TypeCaster/NullTypeCaster.php new file mode 100644 index 0000000..24079a7 --- /dev/null +++ b/src/TypeCaster/NullTypeCaster.php @@ -0,0 +1,62 @@ +isAllowNull($context->getReflectionType())) { + return Result::fail(); + } + + if ( + ($this->null && $value === null) + || ($this->emptyString && $value === '') + || ($this->emptyArray && $value === []) + ) { + return Result::success(null); + } + + return Result::fail(); + } + + private function isAllowNull(?ReflectionType $type): bool + { + if ($type === null) { + return true; + } + + if ($type instanceof ReflectionNamedType) { + return $type->allowsNull(); + } + + if ($type instanceof ReflectionUnionType) { + /** @psalm-suppress RedundantConditionGivenDocblockType Needed for PHP less than 8.2 */ + foreach ($type->getTypes() as $subtype) { + if ($subtype instanceof ReflectionNamedType && $type->allowsNull()) { + return true; + } + } + } + + return false; + } +} diff --git a/tests/TypeCaster/NullTypeCasterTest.php b/tests/TypeCaster/NullTypeCasterTest.php new file mode 100644 index 0000000..1859c38 --- /dev/null +++ b/tests/TypeCaster/NullTypeCasterTest.php @@ -0,0 +1,158 @@ + [ + true, + new NullTypeCaster(), + null, + fn($a) => null, + ], + 'default, null to ?int' => [ + true, + new NullTypeCaster(), + null, + fn(?int $a) => null, + ], + 'default, null to int' => [ + false, + new NullTypeCaster(), + null, + fn(int $a) => null, + ], + 'default, empty string to ?string' => [ + false, + new NullTypeCaster(), + '', + fn(?string $a) => null, + ], + 'default, empty array to ?array' => [ + false, + new NullTypeCaster(), + [], + fn(?array $a) => null, + ], + 'default, empty array to array|string|null' => [ + false, + new NullTypeCaster(), + [], + fn(array|string|null $a) => null, + ], + 'null=false, null to non-type' => [ + false, + new NullTypeCaster(null: false), + null, + fn($a) => null, + ], + 'null=false, null to ?int' => [ + false, + new NullTypeCaster(null: false), + null, + fn(?int $a) => null, + ], + 'null=false, null to int|string|null' => [ + false, + new NullTypeCaster(null: false), + null, + fn(int|string|null $a) => null, + ], + 'emptyString=true, empty string to non-type' => [ + true, + new NullTypeCaster(emptyString: true), + '', + fn($a) => null, + ], + 'emptyString=true, empty string to ?string' => [ + true, + new NullTypeCaster(emptyString: true), + '', + fn(?string $a) => null, + ], + 'emptyString=true, empty string to string|int|null' => [ + true, + new NullTypeCaster(emptyString: true), + '', + fn(string|int|null $a) => null, + ], + 'emptyString=true, empty string to string' => [ + false, + new NullTypeCaster(emptyString: true), + '', + fn(string $a) => null, + ], + 'emptyString=true, empty string to string|int' => [ + false, + new NullTypeCaster(emptyString: true), + '', + fn(string|int $a) => null, + ], + 'emptyArray=true, empty array to non-type' => [ + true, + new NullTypeCaster(emptyArray: true), + [], + fn($a) => null, + ], + 'emptyArray=true, empty array to ?array' => [ + true, + new NullTypeCaster(emptyArray: true), + [], + fn(?array $a) => null, + ], + 'emptyArray=true, empty array to array|string|null' => [ + true, + new NullTypeCaster(emptyArray: true), + [], + fn(array|string|null $a) => null, + ], + 'emptyArray=true, empty array to array' => [ + false, + new NullTypeCaster(emptyArray: true), + [], + fn(array $a) => null, + ], + 'emptyArray=true, empty array to array|string' => [ + false, + new NullTypeCaster(emptyArray: true), + [], + fn(array|string $a) => null, + ], + ]; + } + + /** + * @dataProvider dataBase + */ + public function testBase(bool $success, NullTypeCaster $typeCaster, mixed $value, Closure $closure): void + { + $context = TestHelper::createTypeCastContext($closure); + + $result = $typeCaster->cast($value, $context); + + $this->assertSame($success, $result->isResolved()); + if ($success) { + $this->assertNull($result->getValue()); + } + } + + public function testConstructor(): void + { + $typeCaster = new NullTypeCaster(); + $context = TestHelper::createTypeCastContext(fn($a) => null); + + $result = $typeCaster->cast('hello', $context); + + $this->assertSame(false, $result->isResolved()); + } +}