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 ToArrayOfStrings parameter attribute #94

Merged
merged 10 commits into from
Aug 23, 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
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);
}
}
Loading