Skip to content

Commit

Permalink
Add support for collections (#90)
Browse files Browse the repository at this point in the history
undefined
  • Loading branch information
arogachev authored Aug 7, 2024
1 parent ff7d278 commit d7febf6
Show file tree
Hide file tree
Showing 21 changed files with 556 additions and 16 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 1.3.0 under development

- New #49: Add `Collection` PHP attribute (@arogachev)
- New #49: Add hydrator dependency and `withHydrator()` method to `ParameterAttributesHandler` (@arogachev)
- New #49: Add hydrator dependency and `getHydrator()` method to `ParameterAttributeResolveContext` (@arogachev)
- Enh #85: Allow to hydrate non-initialized readonly properties (@vjik)

## 1.2.0 April 03, 2024
Expand Down
59 changes: 49 additions & 10 deletions docs/guide/en/typecasting.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ $lock = $hydrator->create(Lock::class, ['name' => 'The lock', 'isLocked' => 1]);
You can adjust type-casting by passing a type-caster to the hydrator:

```php
use Yiisoft\Hydrator\Hydrator;
use Yiisoft\Hydrator\TypeCaster\CompositeTypeCaster;
use Yiisoft\Hydrator\TypeCaster\PhpNativeTypeCaster;
use Yiisoft\Hydrator\TypeCaster\HydratorTypeCaster
use Yiisoft\Hydrator\TypeCaster\HydratorTypeCaster;

$typeCaster = new CompositeTypeCaster(
new PhpNativeTypeCaster(),
Expand Down Expand Up @@ -119,6 +120,8 @@ echo $post->getAuthor()->getNickName();

## Using attributes

### `ToString`

To cast a value to string explicitly, you can use `ToString` attribute:

```php
Expand All @@ -139,6 +142,28 @@ $money = $hydrator->create(Money::class, [
]);
```

### `Trim` / `LeftTrim` / `RightTrim`

To strip whitespace (or other characters) from the beginning and/or end of a resolved string value, you can use `Trim`,
`LeftTrim` or `RightTrim` attributes:

```php
use DateTimeImmutable;
use Yiisoft\Hydrator\Attribute\Parameter\Trim;

class Person
{
public function __construct(
#[Trim] // ' John ' → 'John'
private ?string $name = null,
) {}
}

$person = $hydrator->create(Person::class, ['name' => ' John ']);
```

### `ToDatetime`

To cast a value to `DateTimeImmutable` or `DateTime` object explicitly, you can use `ToDateTime` attribute:

```php
Expand All @@ -156,20 +181,34 @@ class Person
$person = $hydrator->create(Person::class, ['birthday' => '27.01.1986']);
```

To strip whitespace (or other characters) from the beginning and/or end of a resolved string value, you can use `Trim`,
`LeftTrim` or `RightTrim` attributes:
### `Collection`

Hydrator supports collections via `Collection` attribute. The class name of related collection must be specified:

```php
use DateTimeImmutable;
use Yiisoft\Hydrator\Attribute\Parameter\Trim;
final class PostCategory
{
public function __construct(
#[Collection(Post::class)]
private array $posts = [],
) {
}
}

class Person
final class Post
{
public function __construct(
#[Trim] // ' John ' → 'John'
private ?string $name = null,
) {}
private string $name,
private string $description = '',
) {
}
}

$person = $hydrator->create(Person::class, ['name' => ' John ']);
$category = $hydrator->create(
PostCategory::class,
[
['name' => 'Post 1'],
['name' => 'Post 2', 'description' => 'Description for post 2'],
],
);
```
28 changes: 28 additions & 0 deletions src/Attribute/Parameter/Collection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Hydrator\Attribute\Parameter;

use Attribute;

/**
* Converts the resolved value to array of instances of the class specified in {@see Collection::$className}.
* Non-resolved and invalid values are skipped.
*/
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER | Attribute::IS_REPEATABLE)]
final class Collection implements ParameterAttributeInterface
{
/**
* @psalm-param class-string $className
*/
public function __construct(
public readonly string $className,
) {
}

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

declare(strict_types=1);

namespace Yiisoft\Hydrator\Attribute\Parameter;

use Yiisoft\Hydrator\AttributeHandling\Exception\UnexpectedAttributeException;
use Yiisoft\Hydrator\AttributeHandling\ParameterAttributeResolveContext;
use Yiisoft\Hydrator\DataInterface;
use Yiisoft\Hydrator\Exception\NonInstantiableException;
use Yiisoft\Hydrator\Result;

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

if (!$context->isResolved()) {
return Result::fail();
}

$resolvedValue = $context->getResolvedValue();
if (!is_iterable($resolvedValue)) {
return Result::fail();
}

$collection = [];
foreach ($resolvedValue as $item) {
if (!is_array($item) && !$item instanceof DataInterface) {
continue;
}

try {
$collection[] = $context->getHydrator()->create($attribute->className, $item);
} catch (NonInstantiableException) {
continue;
}
}

return Result::success($collection);
}
}
13 changes: 13 additions & 0 deletions src/AttributeHandling/ParameterAttributeResolveContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

namespace Yiisoft\Hydrator\AttributeHandling;

use LogicException;
use ReflectionParameter;
use ReflectionProperty;
use Yiisoft\Hydrator\DataInterface;
use Yiisoft\Hydrator\HydratorInterface;
use Yiisoft\Hydrator\Result;

/**
Expand All @@ -18,11 +20,13 @@ final class ParameterAttributeResolveContext
* @param ReflectionParameter|ReflectionProperty $parameter Resolved parameter or property reflection.
* @param Result $resolveResult The resolved value object.
* @param DataInterface $data Data to be used for resolving.
* @param ?HydratorInterface Hydrator instance.
*/
public function __construct(
private ReflectionParameter|ReflectionProperty $parameter,
private Result $resolveResult,
private DataInterface $data,
private ?HydratorInterface $hydrator = null,
) {
}

Expand Down Expand Up @@ -66,4 +70,13 @@ public function getData(): DataInterface
{
return $this->data;
}

public function getHydrator(): HydratorInterface
{
if ($this->hydrator === null) {
throw new LogicException('Hydrator is not set in parameter attribute resolve context.');
}

return $this->hydrator;
}
}
16 changes: 15 additions & 1 deletion src/AttributeHandling/ParameterAttributesHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Yiisoft\Hydrator\AttributeHandling;

use LogicException;
use ReflectionAttribute;
use ReflectionParameter;
use ReflectionProperty;
Expand All @@ -13,6 +14,7 @@
use Yiisoft\Hydrator\Attribute\Parameter\ParameterAttributeResolverInterface;
use Yiisoft\Hydrator\ArrayData;
use Yiisoft\Hydrator\DataInterface;
use Yiisoft\Hydrator\HydratorInterface;
use Yiisoft\Hydrator\Result;

/**
Expand All @@ -22,6 +24,7 @@ final class ParameterAttributesHandler
{
public function __construct(
private AttributeResolverFactoryInterface $attributeResolverFactory,
private ?HydratorInterface $hydrator = null,
) {
}

Expand All @@ -40,6 +43,10 @@ public function handle(
?Result $resolveResult = null,
?DataInterface $data = null
): Result {
if ($this->hydrator === null) {
throw new LogicException('Hydrator is not set in parameter attributes handler.');
}

$resolveResult ??= Result::fail();
$data ??= new ArrayData();

Expand All @@ -60,7 +67,7 @@ public function handle(
);
}

$context = new ParameterAttributeResolveContext($parameter, $resolveResult, $data);
$context = new ParameterAttributeResolveContext($parameter, $resolveResult, $data, $this->hydrator);

$tryResolveResult = $resolver->getParameterValue($attribute, $context);
if ($tryResolveResult->isResolved()) {
Expand All @@ -70,4 +77,11 @@ public function handle(

return $resolveResult;
}

public function withHydrator(HydratorInterface $hydrator): self
{
$new = clone $this;
$new->hydrator = $hydrator;
return $new;
}
}
2 changes: 1 addition & 1 deletion src/Hydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public function __construct(

$attributeResolverFactory ??= new ReflectionAttributeResolverFactory();
$this->dataAttributesHandler = new DataAttributesHandler($attributeResolverFactory);
$this->parameterAttributesHandler = new ParameterAttributesHandler($attributeResolverFactory);
$this->parameterAttributesHandler = new ParameterAttributesHandler($attributeResolverFactory, $this);

$this->objectFactory = $objectFactory ?? new ReflectionObjectFactory();

Expand Down
Loading

0 comments on commit d7febf6

Please sign in to comment.