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 support for collections #90

Merged
merged 31 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
df3e80c
Add support for collections
arogachev Jul 29, 2024
07d33fb
Apply fixes from StyleCI
StyleCIBot Jul 29, 2024
bc54239
Fix unit tests
arogachev Jul 31, 2024
013f911
Merge remote-tracking branch 'origin/collection-support' into collect…
arogachev Jul 31, 2024
909a539
Fix Psalm
arogachev Jul 31, 2024
f04c357
WIP
arogachev Aug 1, 2024
80b6d41
Add PHPDoc [skip ci]
arogachev Aug 2, 2024
4eba0b3
More tests
arogachev Aug 2, 2024
27ae888
Apply fixes from StyleCI
StyleCIBot Aug 2, 2024
8f7162c
Update CHANGELOG [skip ci]
arogachev Aug 2, 2024
0b24aac
Merge remote-tracking branch 'origin/collection-support' into collect…
arogachev Aug 2, 2024
71a9fee
Update guide
arogachev Aug 2, 2024
be18d6f
Make data valid in tests
arogachev Aug 2, 2024
b38c414
Update src/Attribute/Parameter/CollectionResolver.php
arogachev Aug 5, 2024
4174c74
Prevent BC break, use LogicException
arogachev Aug 5, 2024
8345040
Merge remote-tracking branch 'origin/collection-support' into collect…
arogachev Aug 5, 2024
690a652
Microoptimization
arogachev Aug 5, 2024
9c76799
Do not use separate method
arogachev Aug 5, 2024
4b839e4
Add method `withHydrator()`
arogachev Aug 5, 2024
163dbab
Review fix [skip ci]
arogachev Aug 5, 2024
4e37470
Headers [skip ci]
arogachev Aug 5, 2024
5095923
Review fix
arogachev Aug 5, 2024
5a4f492
self
arogachev Aug 5, 2024
b9fbdd8
Fix typo [skip ci]
arogachev Aug 5, 2024
61875bf
Update CHANGELOG.md
arogachev Aug 5, 2024
e7bb65b
Review fix [skip ci]
arogachev Aug 5, 2024
fee42c2
Merge remote-tracking branch 'origin/collection-support' into collect…
arogachev Aug 5, 2024
fc0f3af
Logic fix
arogachev Aug 5, 2024
4ef73fe
Handle noninstatiable exception
arogachev Aug 5, 2024
6fccfd3
break
arogachev Aug 5, 2024
a018aa4
Fix
arogachev Aug 5, 2024
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.0 under development

- New #49: Add support for collections (@arogachev)
arogachev marked this conversation as resolved.
Show resolved Hide resolved
- Enh #85: Allow to hydrate non-initialized readonly properties (@vjik)

## 1.2.0 April 03, 2024
Expand Down
117 changes: 107 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(),
arogachev marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -119,6 +120,8 @@ echo $post->getAuthor()->getNickName();

## Using attributes

### Strings
arogachev marked this conversation as resolved.
Show resolved Hide resolved

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

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

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it better to name these TrimLeft/TrimRight?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your suggestion. I've created separate issue - #91, because it's out of scope of current PR.


```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 ']);
```

### Datetime

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

```php
Expand All @@ -156,20 +179,94 @@ 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:
### Collections

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'],
],
);
```

One-to-many relations can be nested and combined with one-to-one relations:
arogachev marked this conversation as resolved.
Show resolved Hide resolved

```php
final class ChartSet
{
public function __construct(
#[Collection(Chart::class)]
private array $charts = [],
) {
}
}

final class Chart
{
public function __construct(
#[Collection(Point::class)]
private array $points,
) {
}
}

final class Point
{
public function __construct(
private Coordinates $coordinates,
private array $rgb,
) {
}
}

final class Coordinates
{
public function __construct(
private int $x,
private int $y,
) {
}
}

$object = $hydrator->create(
ChartSet::class,
[
'charts' => [
[
'points' => [
['coordinates' => ['x' => 1, 'y' => 1], 'rgb' => [0, 0, 0]],
['coordinates' => ['x' => 2, 'y' => 2], 'rgb' => [0, 0, 0]],
],
],
[
'points' => [
['coordinates' => ['x' => 3, 'y' => 3], 'rgb' => [0, 0, 0]],
['coordinates' => ['x' => 4, 'y' => 4], 'rgb' => [0, 0, 0]],
],
],
],
],
);
```
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;
}
}
42 changes: 42 additions & 0 deletions src/Attribute/Parameter/CollectionResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?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\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 ($context->getResolvedValue() as $item) {
arogachev marked this conversation as resolved.
Show resolved Hide resolved
if (!is_array($item) && !$item instanceof DataInterface) {
return Result::fail();
}

$collection[] = $context->getHydrator()->create($attribute->className, $item);
}

return Result::success($collection);
}
}
7 changes: 7 additions & 0 deletions src/AttributeHandling/ParameterAttributeResolveContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use ReflectionParameter;
use ReflectionProperty;
use Yiisoft\Hydrator\DataInterface;
use Yiisoft\Hydrator\HydratorInterface;
use Yiisoft\Hydrator\Result;

/**
Expand All @@ -23,6 +24,7 @@ public function __construct(
private ReflectionParameter|ReflectionProperty $parameter,
private Result $resolveResult,
private DataInterface $data,
private HydratorInterface $hydrator,
arogachev marked this conversation as resolved.
Show resolved Hide resolved
) {
}

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

public function getHydrator(): HydratorInterface
{
return $this->hydrator;
}
}
19 changes: 18 additions & 1 deletion src/AttributeHandling/ParameterAttributesHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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 +23,7 @@ final class ParameterAttributesHandler
{
public function __construct(
private AttributeResolverFactoryInterface $attributeResolverFactory,
private ?HydratorInterface $hydrator = null,
arogachev marked this conversation as resolved.
Show resolved Hide resolved
) {
}

Expand Down Expand Up @@ -60,7 +62,8 @@ public function handle(
);
}

$context = new ParameterAttributeResolveContext($parameter, $resolveResult, $data);
$this->requireHydrator();
arogachev marked this conversation as resolved.
Show resolved Hide resolved
$context = new ParameterAttributeResolveContext($parameter, $resolveResult, $data, $this->hydrator);

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

return $resolveResult;
}

/**
* Ensure that validator is set in parameter attributes handler.
*
* @psalm-assert HydratorInterface $this->hydrator
*
* @throws RuntimeException If hydrator is not set in parameter attributes handler.
*/
private function requireHydrator(): void
arogachev marked this conversation as resolved.
Show resolved Hide resolved
{
if ($this->hydrator === null) {
throw new RuntimeException('Hydrator is not set in parameter attributes handler.');
}
}
}
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
Loading