Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add backed enumeration support to Collection #93

Merged
merged 5 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
samdark marked this conversation as resolved.
Show resolved Hide resolved
{
/** @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';
}
Loading