From d95f188d53d8230301317582d8d98a18eb5ea4d1 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 9 Aug 2024 17:01:36 +0300 Subject: [PATCH 1/5] implement --- .../Parameter/CollectionResolver.php | 71 +++++++- tests/Attribute/Parameter/CollectionTest.php | 168 ++++++++++++------ tests/Support/IntegerEnum.php | 12 ++ tests/Support/StringEnum.php | 12 ++ 4 files changed, 196 insertions(+), 67 deletions(-) create mode 100644 tests/Support/IntegerEnum.php create mode 100644 tests/Support/StringEnum.php diff --git a/src/Attribute/Parameter/CollectionResolver.php b/src/Attribute/Parameter/CollectionResolver.php index 903323e..5cf6fba 100644 --- a/src/Attribute/Parameter/CollectionResolver.php +++ b/src/Attribute/Parameter/CollectionResolver.php @@ -4,6 +4,9 @@ namespace Yiisoft\Hydrator\Attribute\Parameter; +use BackedEnum; +use ReflectionEnum; +use ReflectionNamedType; use Yiisoft\Hydrator\AttributeHandling\Exception\UnexpectedAttributeException; use Yiisoft\Hydrator\AttributeHandling\ParameterAttributeResolveContext; use Yiisoft\Hydrator\DataInterface; @@ -29,19 +32,71 @@ public function getParameterValue( return Result::fail(); } + $isBackedEnum = is_a($attribute->className, BackedEnum::class, true); + /** + * If `$isBackedEnum` is true, `$attribute->className` is `BackedEnum` class. + * @psalm-suppress ArgumentTypeCoercion + */ + $isStringBackedEnum = $isBackedEnum && $this->isStringBackedEnum($attribute->className); + $collection = []; - foreach ($resolvedValue as $item) { - if (!is_array($item) && !$item instanceof DataInterface) { - continue; - } - try { - $collection[] = $context->getHydrator()->create($attribute->className, $item); - } catch (NonInstantiableException) { - continue; + if ($isBackedEnum) { + foreach ($resolvedValue as $item) { + try { + /** + * If `$isBackedEnum` is true, `$attribute->className` is `BackedEnum` class. + * @psalm-suppress ArgumentTypeCoercion + */ + $collection[] = $this->createBackedEnum($attribute->className, $isStringBackedEnum, $item); + } catch (NonInstantiableException) { + continue; + } + } + } else { + foreach ($resolvedValue as $item) { + if (!is_array($item) && !$item instanceof DataInterface) { + continue; + } + try { + $collection[] = $context->getHydrator()->create($attribute->className, $item); + } catch (NonInstantiableException) { + continue; + } } } + return Result::success($collection); } + + /** + * @psalm-param class-string $className + * @throws NonInstantiableException + */ + private function createBackedEnum(string $className, bool $isStringBackedEnum, mixed $value): BackedEnum + { + if ($value instanceof $className) { + return $value; + } + + if (is_string($value) || is_int($value)) { + $enum = $className::tryFrom($isStringBackedEnum ? (string) $value : (int) $value); + if ($enum !== null) { + return $enum; + } + } + + throw new NonInstantiableException(); + } + + /** + * @psalm-param class-string $className + */ + private function isStringBackedEnum(string $className): bool + { + /** @var ReflectionNamedType $backingType */ + $backingType = (new ReflectionEnum($className))->getBackingType(); + return $backingType->getName() === 'string'; + } } diff --git a/tests/Attribute/Parameter/CollectionTest.php b/tests/Attribute/Parameter/CollectionTest.php index 8a2a8d5..28db98f 100644 --- a/tests/Attribute/Parameter/CollectionTest.php +++ b/tests/Attribute/Parameter/CollectionTest.php @@ -25,6 +25,8 @@ use Yiisoft\Hydrator\Tests\Support\Classes\CounterClass; use Yiisoft\Hydrator\Tests\Support\Classes\Post\Post; use Yiisoft\Hydrator\Tests\Support\Classes\Post\PostCategory; +use Yiisoft\Hydrator\Tests\Support\IntegerEnum; +use Yiisoft\Hydrator\Tests\Support\StringEnum; use Yiisoft\Hydrator\Tests\Support\TestHelper; use Yiisoft\Test\Support\Container\SimpleContainer; @@ -114,75 +116,123 @@ public function testNonInstantiableValueItem(): void ); } - public static function dataBase(): array + public static function dataBase(): iterable { - return [ - 'basic' => [ - new Collection(Post::class), - [ - ['name' => 'Post 1'], - ['name' => 'Post 2', 'description' => 'Description for post 2'], - ], - [ - new Post(name: 'Post 1'), - new Post(name: 'Post 2', description: 'Description for post 2'), - ], + yield 'basic' => [ + new Collection(Post::class), + [ + ['name' => 'Post 1'], + ['name' => 'Post 2', 'description' => 'Description for post 2'], ], - 'nested, one to one and one to many relations' => [ - new Collection(Chart::class), + [ + new Post(name: 'Post 1'), + new Post(name: 'Post 2', description: 'Description for post 2'), + ], + ]; + yield 'nested, one to one and one to many relations' => [ + new Collection(Chart::class), + [ [ - [ - 'points' => [ - ['coordinates' => ['x' => 1, 'y' => 1], 'rgb' => [255, 0, 0]], - ['coordinates' => ['x' => 2, 'y' => 2], 'rgb' => [255, 0, 0]], - ], - ], - [ - 'points' => [ - ['coordinates' => ['x' => 3, 'y' => 3], 'rgb' => [0, 255, 0]], - ['coordinates' => ['x' => 4, 'y' => 4], 'rgb' => [0, 255, 0]], - ], - ], - [ - 'points' => [ - ['coordinates' => ['x' => 5, 'y' => 5], 'rgb' => [0, 0, 255]], - ['coordinates' => ['x' => 6, 'y' => 6], 'rgb' => [0, 0, 255]], - ], + 'points' => [ + ['coordinates' => ['x' => 1, 'y' => 1], 'rgb' => [255, 0, 0]], + ['coordinates' => ['x' => 2, 'y' => 2], 'rgb' => [255, 0, 0]], ], ], [ - new Chart([ - new Point(new Coordinates(1, 1), [255, 0, 0]), - new Point(new Coordinates(2, 2), [255, 0, 0]), - ]), - new Chart([ - new Point(new Coordinates(3, 3), [0, 255, 0]), - new Point(new Coordinates(4, 4), [0, 255, 0]), - ]), - new Chart([ - new Point(new Coordinates(5, 5), [0, 0, 255]), - new Point(new Coordinates(6, 6), [0, 0, 255]), - ]), - ], - ], - 'value item provided by class' => [ - new Collection(Post::class), - [ - ['name' => 'Post 1'], - new class () implements DataInterface { - public function getValue(string $name): Result - { - $value = $name === 'name' ? 'Post 2' : 'Description for post 2'; - - return Result::success($value); - } - }, + 'points' => [ + ['coordinates' => ['x' => 3, 'y' => 3], 'rgb' => [0, 255, 0]], + ['coordinates' => ['x' => 4, 'y' => 4], 'rgb' => [0, 255, 0]], + ], ], [ - new Post(name: 'Post 1'), - new Post(name: 'Post 2', description: 'Description for post 2'), + 'points' => [ + ['coordinates' => ['x' => 5, 'y' => 5], 'rgb' => [0, 0, 255]], + ['coordinates' => ['x' => 6, 'y' => 6], 'rgb' => [0, 0, 255]], + ], ], ], + [ + new Chart([ + new Point(new Coordinates(1, 1), [255, 0, 0]), + new Point(new Coordinates(2, 2), [255, 0, 0]), + ]), + new Chart([ + new Point(new Coordinates(3, 3), [0, 255, 0]), + new Point(new Coordinates(4, 4), [0, 255, 0]), + ]), + new Chart([ + new Point(new Coordinates(5, 5), [0, 0, 255]), + new Point(new Coordinates(6, 6), [0, 0, 255]), + ]), + ], + ]; + yield 'value item provided by class' => [ + new Collection(Post::class), + [ + ['name' => 'Post 1'], + new class () implements DataInterface { + public function getValue(string $name): Result + { + $value = $name === 'name' ? 'Post 2' : 'Description for post 2'; + + return Result::success($value); + } + }, + ], + [ + new Post(name: 'Post 1'), + new Post(name: 'Post 2', description: 'Description for post 2'), + ], + ]; + yield [ + new Collection(StringEnum::class), + [], + [], + ]; + yield [ + new Collection(StringEnum::class), + ['A'], + [], + ]; + yield [ + new Collection(StringEnum::class), + ['one', 'three'], + [StringEnum::A, StringEnum::C], + ]; + yield [ + new Collection(StringEnum::class), + ['one', 'four', 'three'], + [StringEnum::A, StringEnum::C], + ]; + yield [ + new Collection(StringEnum::class), + ['one', 2, 'three'], + [StringEnum::A, StringEnum::C], + ]; + yield [ + new Collection(IntegerEnum::class), + [], + [], + ]; + yield [ + new Collection(IntegerEnum::class), + ['A'], + [], + ]; + yield [ + new Collection(IntegerEnum::class), + [1, 3], + [IntegerEnum::A, IntegerEnum::C], + ]; + yield [ + new Collection(IntegerEnum::class), + [1, 4, 3], + [IntegerEnum::A, IntegerEnum::C], + ]; + yield [ + new Collection(IntegerEnum::class), + [1, 'two', 3], + [IntegerEnum::A, IntegerEnum::C], ]; } diff --git a/tests/Support/IntegerEnum.php b/tests/Support/IntegerEnum.php new file mode 100644 index 0000000..7170f97 --- /dev/null +++ b/tests/Support/IntegerEnum.php @@ -0,0 +1,12 @@ + Date: Fri, 9 Aug 2024 17:09:10 +0300 Subject: [PATCH 2/5] refactor --- .../Parameter/CollectionResolver.php | 90 ++++++++++--------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/src/Attribute/Parameter/CollectionResolver.php b/src/Attribute/Parameter/CollectionResolver.php index 5cf6fba..4bc7320 100644 --- a/src/Attribute/Parameter/CollectionResolver.php +++ b/src/Attribute/Parameter/CollectionResolver.php @@ -11,6 +11,7 @@ use Yiisoft\Hydrator\AttributeHandling\ParameterAttributeResolveContext; use Yiisoft\Hydrator\DataInterface; use Yiisoft\Hydrator\Exception\NonInstantiableException; +use Yiisoft\Hydrator\HydratorInterface; use Yiisoft\Hydrator\Result; final class CollectionResolver implements ParameterAttributeResolverInterface @@ -33,61 +34,70 @@ public function getParameterValue( } $isBackedEnum = is_a($attribute->className, BackedEnum::class, true); - /** - * If `$isBackedEnum` is true, `$attribute->className` is `BackedEnum` class. - * @psalm-suppress ArgumentTypeCoercion - */ - $isStringBackedEnum = $isBackedEnum && $this->isStringBackedEnum($attribute->className); - - $collection = []; if ($isBackedEnum) { - foreach ($resolvedValue as $item) { - try { - /** - * If `$isBackedEnum` is true, `$attribute->className` is `BackedEnum` class. - * @psalm-suppress ArgumentTypeCoercion - */ - $collection[] = $this->createBackedEnum($attribute->className, $isStringBackedEnum, $item); - } catch (NonInstantiableException) { - continue; - } - } + /** + * If `$isBackedEnum` is true, `$attribute->className` is `BackedEnum` class. + * @psalm-suppress ArgumentTypeCoercion + */ + $collection = $this->createCollectionOfBackedEnums($resolvedValue, $attribute->className); } else { - foreach ($resolvedValue as $item) { - if (!is_array($item) && !$item instanceof DataInterface) { - continue; - } - try { - $collection[] = $context->getHydrator()->create($attribute->className, $item); - } catch (NonInstantiableException) { - continue; - } - } + $collection = $this->createCollectionOfObjects( + $resolvedValue, + $context->getHydrator(), + $attribute->className + ); } - return Result::success($collection); } + /** + * @psalm-param class-string $className + * @return object[] + */ + private function createCollectionOfObjects( + iterable $resolvedValue, + HydratorInterface $hydrator, + string $className + ): array { + $collection = []; + foreach ($resolvedValue as $item) { + if (!is_array($item) && !$item instanceof DataInterface) { + continue; + } + + try { + $collection[] = $hydrator->create($className, $item); + } catch (NonInstantiableException) { + continue; + } + } + return $collection; + } + /** * @psalm-param class-string $className - * @throws NonInstantiableException + * @return BackedEnum[] */ - private function createBackedEnum(string $className, bool $isStringBackedEnum, mixed $value): BackedEnum + private function createCollectionOfBackedEnums(iterable $resolvedValue, string $className): array { - if ($value instanceof $className) { - return $value; - } + $collection = []; + $isStringBackedEnum = $this->isStringBackedEnum($className); + foreach ($resolvedValue as $item) { + if ($item instanceof $className) { + $collection[] = $item; + continue; + } - if (is_string($value) || is_int($value)) { - $enum = $className::tryFrom($isStringBackedEnum ? (string) $value : (int) $value); - if ($enum !== null) { - return $enum; + if (is_string($item) || is_int($item)) { + $enum = $className::tryFrom($isStringBackedEnum ? (string) $item : (int) $item); + if ($enum !== null) { + $collection[] = $enum; + } } } - - throw new NonInstantiableException(); + return $collection; } /** From 4b81b5b7653432b7fc176e42c922791961e92dee Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 9 Aug 2024 17:10:50 +0300 Subject: [PATCH 3/5] refactor --- src/Attribute/Parameter/CollectionResolver.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Attribute/Parameter/CollectionResolver.php b/src/Attribute/Parameter/CollectionResolver.php index 4bc7320..1071942 100644 --- a/src/Attribute/Parameter/CollectionResolver.php +++ b/src/Attribute/Parameter/CollectionResolver.php @@ -33,12 +33,9 @@ public function getParameterValue( return Result::fail(); } - $isBackedEnum = is_a($attribute->className, BackedEnum::class, true); - - if ($isBackedEnum) { + if (is_a($attribute->className, BackedEnum::class, true)) { /** - * If `$isBackedEnum` is true, `$attribute->className` is `BackedEnum` class. - * @psalm-suppress ArgumentTypeCoercion + * @psalm-suppress ArgumentTypeCoercion Because class name is backed enumeration name. */ $collection = $this->createCollectionOfBackedEnums($resolvedValue, $attribute->className); } else { From 80c921436a8634b2fa1b9dcf4b86a7fb989825bb Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 9 Aug 2024 17:12:43 +0300 Subject: [PATCH 4/5] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69c840f..d89a095 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 1.3.1 under development -- no changes in this release. +- Enh #93: Add backed enumeration support to `Collection` (@vjik) ## 1.3.0 August 07, 2024 From 351cd68db688bc9f2baa4980c70a6393fede4607 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 9 Aug 2024 17:17:16 +0300 Subject: [PATCH 5/5] more tests --- tests/Attribute/Parameter/CollectionTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Attribute/Parameter/CollectionTest.php b/tests/Attribute/Parameter/CollectionTest.php index 28db98f..2a0efee 100644 --- a/tests/Attribute/Parameter/CollectionTest.php +++ b/tests/Attribute/Parameter/CollectionTest.php @@ -209,6 +209,11 @@ public function getValue(string $name): Result ['one', 2, 'three'], [StringEnum::A, StringEnum::C], ]; + yield [ + new Collection(StringEnum::class), + [StringEnum::A, StringEnum::C], + [StringEnum::A, StringEnum::C], + ]; yield [ new Collection(IntegerEnum::class), [], @@ -234,6 +239,11 @@ public function getValue(string $name): Result [1, 'two', 3], [IntegerEnum::A, IntegerEnum::C], ]; + yield [ + new Collection(IntegerEnum::class), + [IntegerEnum::A, IntegerEnum::C], + [IntegerEnum::A, IntegerEnum::C], + ]; } #[DataProvider('dataBase')]