diff --git a/CHANGELOG.md b/CHANGELOG.md index d89a095..6e678f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 1.3.1 under development +- New #94: Add `ToArrayOfStrings` parameter attribute (@vjik) - Enh #93: Add backed enumeration support to `Collection` (@vjik) ## 1.3.0 August 07, 2024 diff --git a/docs/guide/en/typecasting.md b/docs/guide/en/typecasting.md index ccda587..e41043b 100644 --- a/docs/guide/en/typecasting.md +++ b/docs/guide/en/typecasting.md @@ -212,3 +212,28 @@ $category = $hydrator->create( ], ); ``` + +### `ToArrayOfStrings` + +Use `ToArrayOfStrings` attribute to cast a value to an array of strings: + +```php +use Yiisoft\Hydrator\Attribute\Parameter\ToArrayOfStrings; + +final class Post +{ + #[ToArrayOfStrings(separator: ',')] + public array $tags = []; +} +``` + +Value of `tags` will be cast to an array of strings by splitting it by `,`. For example, string `news,city,hot` will be +converted to array `['news', 'city', 'hot']`. + +Attribute parameters: + +- `trim` — trim each string of array (boolean, default `false`); +- `removeEmpty` — remove empty strings from array (boolean, default `false`); +- `splitResolvedValue` — split resolved value by separator (boolean, default `true`); +- `separator` — the boundary string (default, `\R`), it's a part of regular expression so should be taken into account + or properly escaped with `preg_quote()`. diff --git a/src/Attribute/Parameter/ToArrayOfStrings.php b/src/Attribute/Parameter/ToArrayOfStrings.php new file mode 100644 index 0000000..8fde72f --- /dev/null +++ b/src/Attribute/Parameter/ToArrayOfStrings.php @@ -0,0 +1,34 @@ +isResolved()) { + return Result::fail(); + } + + $resolvedValue = $context->getResolvedValue(); + if (is_iterable($resolvedValue)) { + $array = array_map( + $this->castValueToString(...), + $resolvedValue instanceof Traversable ? iterator_to_array($resolvedValue) : $resolvedValue + ); + } else { + $value = $this->castValueToString($resolvedValue); + $array = $attribute->splitResolvedValue + ? preg_split('~' . $attribute->separator . '~u', $value) + : [$value]; + } + + if ($attribute->trim) { + $array = array_map(trim(...), $array); + } + + if ($attribute->removeEmpty) { + $array = array_filter( + $array, + static fn(string $value): bool => $value !== '', + ); + } + + return Result::success($array); + } + + private function castValueToString(mixed $value): string + { + return is_scalar($value) || $value instanceof Stringable ? (string) $value : ''; + } +} diff --git a/tests/Attribute/Parameter/ToArrayOfStringsTest.php b/tests/Attribute/Parameter/ToArrayOfStringsTest.php new file mode 100644 index 0000000..de2eebc --- /dev/null +++ b/tests/Attribute/Parameter/ToArrayOfStringsTest.php @@ -0,0 +1,200 @@ + 'world'], + "hello\n\nworld", + new class () { + #[ToArrayOfStrings(removeEmpty: true)] + public ?array $value = null; + }, + ]; + yield [ + ['hello', '', ' world', ' good '], + "hello\n\n world\n good ", + new class () { + #[ToArrayOfStrings] + public ?array $value = null; + }, + ]; + yield [ + ['hello', 2 => 'world', 3 => 'good'], + "hello\n\n world\n good ", + new class () { + #[ToArrayOfStrings(trim: true, removeEmpty: true)] + public ?array $value = null; + }, + ]; + yield [ + ['hello', 'world', 'good'], + 'hello,world,good', + new class () { + #[ToArrayOfStrings(separator: ',')] + public ?array $value = null; + }, + ]; + } + + #[DataProvider('dataBase')] + public function testBase(mixed $expectedValue, mixed $value, object $object) + { + (new Hydrator())->hydrate($object, ['value' => $value]); + $this->assertSame($expectedValue, $object->value); + } + + public function testNotResolved(): void + { + $object = new class () { + #[ToArrayOfStrings] + public ?array $value = null; + }; + + (new Hydrator())->hydrate($object); + + $this->assertNull($object->value); + } + + public function testUnexpectedAttributeException(): void + { + $hydrator = new Hydrator( + attributeResolverFactory: new ContainerAttributeResolverFactory( + new SimpleContainer([ + CounterResolver::class => new ToArrayOfStringsResolver(), + ]), + ), + ); + + $object = new CounterClass(); + + $this->expectException(UnexpectedAttributeException::class); + $this->expectExceptionMessage( + 'Expected "' . ToArrayOfStrings::class . '", but "' . Counter::class . '" given.' + ); + $hydrator->hydrate($object); + } +}