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 diff --git a/src/Attribute/Parameter/CollectionResolver.php b/src/Attribute/Parameter/CollectionResolver.php index 903323e..1071942 100644 --- a/src/Attribute/Parameter/CollectionResolver.php +++ b/src/Attribute/Parameter/CollectionResolver.php @@ -4,10 +4,14 @@ 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; use Yiisoft\Hydrator\Exception\NonInstantiableException; +use Yiisoft\Hydrator\HydratorInterface; use Yiisoft\Hydrator\Result; final class CollectionResolver implements ParameterAttributeResolverInterface @@ -29,6 +33,31 @@ public function getParameterValue( return Result::fail(); } + if (is_a($attribute->className, BackedEnum::class, true)) { + /** + * @psalm-suppress ArgumentTypeCoercion Because class name is backed enumeration name. + */ + $collection = $this->createCollectionOfBackedEnums($resolvedValue, $attribute->className); + } else { + $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) { @@ -36,12 +65,45 @@ public function getParameterValue( } try { - $collection[] = $context->getHydrator()->create($attribute->className, $item); + $collection[] = $hydrator->create($className, $item); } catch (NonInstantiableException) { continue; } } + return $collection; + } - return Result::success($collection); + /** + * @psalm-param class-string $className + * @return BackedEnum[] + */ + private function createCollectionOfBackedEnums(iterable $resolvedValue, string $className): array + { + $collection = []; + $isStringBackedEnum = $this->isStringBackedEnum($className); + foreach ($resolvedValue as $item) { + if ($item instanceof $className) { + $collection[] = $item; + continue; + } + + if (is_string($item) || is_int($item)) { + $enum = $className::tryFrom($isStringBackedEnum ? (string) $item : (int) $item); + if ($enum !== null) { + $collection[] = $enum; + } + } + } + return $collection; + } + + /** + * @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..2a0efee 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,133 @@ 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'], + ], + [ + new Post(name: 'Post 1'), + new Post(name: 'Post 2', description: 'Description for post 2'), ], - 'nested, one to one and one to many relations' => [ - new Collection(Chart::class), + ]; + 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' => 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]], - ], - ], - ], - [ - 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(StringEnum::class), + [StringEnum::A, StringEnum::C], + [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], + ]; + yield [ + new Collection(IntegerEnum::class), + [IntegerEnum::A, IntegerEnum::C], + [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 @@ +