Skip to content

Commit

Permalink
Add backed enumeration support to Collection (#93)
Browse files Browse the repository at this point in the history
* implement

* refactor

* refactor

* changelog

* more tests
  • Loading branch information
vjik authored Aug 12, 2024
1 parent fa2de89 commit 75e3116
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 62 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
66 changes: 64 additions & 2 deletions src/Attribute/Parameter/CollectionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,19 +33,77 @@ 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) {
continue;
}

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<BackedEnum> $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<BackedEnum> $className
*/
private function isStringBackedEnum(string $className): bool
{
/** @var ReflectionNamedType $backingType */
$backingType = (new ReflectionEnum($className))->getBackingType();
return $backingType->getName() === 'string';
}
}
178 changes: 119 additions & 59 deletions tests/Attribute/Parameter/CollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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],
];
}

Expand Down
12 changes: 12 additions & 0 deletions tests/Support/IntegerEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Tests\Support;

enum IntegerEnum: int
{
case A = 1;
case B = 2;
case C = 3;
}
12 changes: 12 additions & 0 deletions tests/Support/StringEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Tests\Support;

enum StringEnum: string
{
case A = 'one';
case B = 'two';
case C = 'three';
}

0 comments on commit 75e3116

Please sign in to comment.