Skip to content

Commit

Permalink
Add ToArrayOfStrings parameter attribute (#94)
Browse files Browse the repository at this point in the history
  • Loading branch information
vjik authored Aug 23, 2024
1 parent 75e3116 commit 80fac1b
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions docs/guide/en/typecasting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.
34 changes: 34 additions & 0 deletions src/Attribute/Parameter/ToArrayOfStrings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Attribute\Parameter;

use Attribute;

/**
* Casts the resolved value to array of strings.
*/
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER | Attribute::IS_REPEATABLE)]
final class ToArrayOfStrings implements ParameterAttributeInterface
{
/**
* @param bool $trim Trim each string of array.
* @param bool $removeEmpty Remove empty strings from array.
* @param bool $splitResolvedValue Split non-array resolved value to array of strings by {@see $separator}.
* @param string $separator The boundary string. It is a part of regular expression
* so should be taken into account or properly escaped with {@see preg_quote()}.
*/
public function __construct(
public readonly bool $trim = false,
public readonly bool $removeEmpty = false,
public readonly bool $splitResolvedValue = true,
public readonly string $separator = '\R',
) {
}

public function getResolver(): string
{
return ToArrayOfStringsResolver::class;
}
}
58 changes: 58 additions & 0 deletions src/Attribute/Parameter/ToArrayOfStringsResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Attribute\Parameter;

use Stringable;
use Traversable;
use Yiisoft\Hydrator\AttributeHandling\Exception\UnexpectedAttributeException;
use Yiisoft\Hydrator\AttributeHandling\ParameterAttributeResolveContext;
use Yiisoft\Hydrator\Result;

final class ToArrayOfStringsResolver implements ParameterAttributeResolverInterface
{
public function getParameterValue(
ParameterAttributeInterface $attribute,
ParameterAttributeResolveContext $context
): Result {
if (!$attribute instanceof ToArrayOfStrings) {
throw new UnexpectedAttributeException(ToArrayOfStrings::class, $attribute);
}

if (!$context->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 : '';
}
}
200 changes: 200 additions & 0 deletions tests/Attribute/Parameter/ToArrayOfStringsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Tests\Attribute\Parameter;

use ArrayObject;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use stdClass;
use Yiisoft\Hydrator\Attribute\Parameter\ToArrayOfStrings;
use Yiisoft\Hydrator\Attribute\Parameter\ToArrayOfStringsResolver;
use Yiisoft\Hydrator\AttributeHandling\Exception\UnexpectedAttributeException;
use Yiisoft\Hydrator\AttributeHandling\ResolverFactory\ContainerAttributeResolverFactory;
use Yiisoft\Hydrator\Hydrator;
use Yiisoft\Hydrator\Tests\Support\Attribute\Counter;
use Yiisoft\Hydrator\Tests\Support\Attribute\CounterResolver;
use Yiisoft\Hydrator\Tests\Support\Classes\CounterClass;
use Yiisoft\Test\Support\Container\SimpleContainer;

final class ToArrayOfStringsTest extends TestCase
{
public static function dataBase(): iterable
{
yield [
[],
[],
new class () {
#[ToArrayOfStrings]
public ?array $value = null;
},
];
yield [
[''],
'',
new class () {
#[ToArrayOfStrings]
public ?array $value = null;
},
];
yield [
[''],
new stdClass(),
new class () {
#[ToArrayOfStrings]
public ?array $value = null;
},
];
yield [
['hello'],
'hello',
new class () {
#[ToArrayOfStrings]
public ?array $value = null;
},
];
yield [
['hello'],
['hello'],
new class () {
#[ToArrayOfStrings]
public ?array $value = null;
},
];
yield [
['hello '],
'hello ',
new class () {
#[ToArrayOfStrings]
public ?array $value = null;
},
];
yield [
['hello'],
'hello ',
new class () {
#[ToArrayOfStrings(trim: true)]
public ?array $value = null;
},
];
yield [
['hello', 'world'],
"hello\nworld",
new class () {
#[ToArrayOfStrings]
public ?array $value = null;
},
];
yield [
['hello', 'world'],
['hello', 'world'],
new class () {
#[ToArrayOfStrings]
public ?array $value = null;
},
];
yield [
['hello', '42', '1', '2.4'],
['hello', 42, true, 2.4],
new class () {
#[ToArrayOfStrings]
public ?array $value = null;
},
];
yield [
['hello', 'world'],
new ArrayObject(['hello', 'world']),
new class () {
#[ToArrayOfStrings]
public ?array $value = null;
},
];
yield [
["hello\nworld"],
"hello\nworld",
new class () {
#[ToArrayOfStrings(splitResolvedValue: false)]
public ?array $value = null;
},
];
yield [
['hello', '', 'world'],
"hello\n\nworld",
new class () {
#[ToArrayOfStrings]
public ?array $value = null;
},
];
yield [
['hello', 2 => '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);
}
}

0 comments on commit 80fac1b

Please sign in to comment.