Skip to content

Commit

Permalink
Merge pull request #28 from sunrise-php/release/v3.2.0
Browse files Browse the repository at this point in the history
v3.2.0
  • Loading branch information
fenric authored Oct 4, 2023
2 parents 6c9276d + 0096868 commit 76dcd05
Show file tree
Hide file tree
Showing 58 changed files with 1,989 additions and 880 deletions.
120 changes: 112 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![Latest Stable Version](https://poser.pugx.org/sunrise/hydrator/v/stable?format=flat)](https://packagist.org/packages/sunrise/hydrator)
[![License](https://poser.pugx.org/sunrise/hydrator/license?format=flat)](https://packagist.org/packages/sunrise/hydrator)

**php**, **dto**, **hydrator**, **mapper**, **data-mapper**, **model-mapper**
**php**, **dto**, **hydrator**, **mapper**, **populator**, **data-mapper**

---

Expand All @@ -33,6 +33,7 @@ composer require sunrise/hydrator
* * [UID](#uid)
* * [Enumeration](#enumeration)
* * [Relationship](#relationship)
* * [Custom type](#support-for-custom-types)
* [Ignored property](#ignored-property)
* [Property alias](#property-alias)
* [Error handling](#error-handling)
Expand Down Expand Up @@ -213,35 +214,69 @@ By default, this property accepts an array with any data. However, it can also b
public readonly array $value;
```

> In other words, the **Subtype** annotation can contain the same types as the types of class properties...
Having an unlimited number of relationships in an array is a potentially bad idea as it can lead to memory leaks. To avoid this, it is recommended to limit such an array, as shown in the example below:

```php
#[\Sunrise\Hydrator\Annotation\Subtype(SomeDto::class, limit: 100)]
public readonly array $value;
```

In addition to arrays, you can use collections, i.e. objects implementing the [ArrayAccess](http://php.net/ArrayAccess) interface, for example:
In addition to arrays, you can also use **collections**, in other words, classes implementing the [ArrayAccess](http://php.net/ArrayAccess) interface, for example:

```php
final class TagDto
{
}
```

```php
final class TagCollection implements ArrayAccess
final class TagCollection implements \ArrayAccess
{
// some code...
}
```

```php
final class CreateProductDto
{
public readonly TagCollection $tags;
public function __construct(
#[\Sunrise\Hydrator\Annotation\Subtype(TagDto::class, limit: 10)]
public readonly TagCollection $tags,
) {
}
}
```

Additionally, you can type the elements of such an array or collection, like this:
Note that for collections, instead of the **Subtype** annotation, you can use typing through its constructor. It is important that there is only one variadic parameter in it. Please refer to the example below:

> Please note that in this case, you take on the responsibility of limiting the collection. To ensure that the hydrator understands when the collection is full, the [offsetSet](https://www.php.net/arrayaccess.offsetset) method should throw an [OverflowException](https://www.php.net/overflowexception).
```php
#[\Sunrise\Hydrator\Annotation\Subtype(DateTimeImmutable::class, limit: 100)]
final class TagCollection implements \ArrayAccess
{
public function __construct(public TagDto ...$tags)
{
}
}
```

```php
final class CreateProductDto
{
public function __construct(
public readonly TagCollection $tags,
) {
}
}
```

In general, remember that regardless of whether arrays or collections are used, their elements can be typed. For example, if you need an array that should consist only of dates, your code should look something like this:

```php
#[\Sunrise\Hydrator\Annotation\Subtype(\DateTimeImmutable::class, limit: 100)]
#[\Sunrise\Hydrator\Annotation\Format('Y-m-d H:i:s')]
public readonly array $tags;
public readonly array $value;
```

This property has no any additional behavior and only accepts arrays.
Expand All @@ -264,6 +299,17 @@ public readonly DateTimeImmutable $value;

Also, please note that if a value in a dataset for this property is represented as an empty string or a string consisting only of whitespace, then the value will be handled as [null](#null).

#### Default timestamp format

```php
use Sunrise\Hydrator\Dictionary\ContextKey;
use Sunrise\Hydrator\Hydrator;

$hydrator = new Hydrator([
ContextKey::TIMESTAMP_FORMAT => 'Y-m-d H:i:s',
]);
```

### Timezone

Only the DateTimeZone type is supported.
Expand All @@ -274,6 +320,17 @@ public readonly DateTimeZone $value;

Also, please note that if a value in a dataset for this property is represented as an empty string or a string consisting only of whitespace, then the value will be handled as [null](#null).

#### Default timezone

```php
use Sunrise\Hydrator\Dictionary\ContextKey;
use Sunrise\Hydrator\Hydrator;

$hydrator = new Hydrator([
ContextKey::TIMEZONE => 'Europe/Kyiv',
]);
```

### UID

```bash
Expand Down Expand Up @@ -305,6 +362,53 @@ public readonly SomeDto $value;

A value in a dataset can only be an array. However, please note that if you need a one-to-many relationship, you should refer to the [array](#array) section for further information.

## Support for custom types

If you need support for a custom type, it is a relatively simple task. Let's write such support for UUID from the [ramsey/uuid](https://github.com/ramsey/uuid) package:

```php
use Sunrise\Hydrator\Exception\InvalidValueException;
use Sunrise\Hydrator\Type;
use Sunrise\Hydrator\TypeConverterInterface;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;

final class UuidTypeConverter implements TypeConverterInterface
{
public function castValue($value, Type $type, array $path): Generator
{
if ($type->getName() <> UuidInterface::class) {
return;
}

if (!\is_string($value)) {
throw InvalidValueException::mustBeString($path);
}

if (!Uuid::isValid($value)) {
throw new InvalidValueException(
'This value is not a valid UUID.',
'c66741c6-e3c0-4522-a8e3-97528d7712a3',
$path,
);
}

yield Uuid::fromString($value);
}

public function getWeight(): int
{
return 31;
}
}
```

Now, let's inform the hydrator about the new type:

```php
$hydrator->addTypeConverter(new UuidTypeConverter());
```

## Ignored property

If you need a property to be ignored and not populated during the object hydration process, use a special annotation like the example below:
Expand Down
7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
"sunrise",
"hydrator",
"mapper",
"populator",
"object-hydrator",
"object-mapper",
"object-populator",
"data-mapper",
"php7",
"php8"
Expand All @@ -30,7 +33,9 @@
"phpstan/phpstan": "^1.10",
"doctrine/annotations": "^2.0",
"symfony/validator": "^5.4",
"symfony/uid": "^5.4"
"symfony/uid": "^5.4",
"ramsey/uuid": "^4.2",
"myclabs/php-enum": "^1.8"
},
"autoload": {
"psr-4": {
Expand Down
51 changes: 51 additions & 0 deletions src/Annotation/Context.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/**
* It's free open-source software released under the MIT License.
*
* @author Anatoly Nekhay <[email protected]>
* @copyright Copyright (c) 2021, Anatoly Nekhay
* @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE
* @link https://github.com/sunrise-php/hydrator
*/

declare(strict_types=1);

namespace Sunrise\Hydrator\Annotation;

use Attribute;

/**
* @Annotation
* @Target({"PROPERTY"})
* @NamedArgumentConstructor
*
* @Attributes({
* @Attribute("value", type="array", required=true),
* })
*
* @since 3.2.0
*/
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
final class Context
{

/**
* The attribute value
*
* @var array<non-empty-string, mixed>
*
* @readonly
*/
public array $value;

/**
* Constructor of the class
*
* @param array<non-empty-string, mixed> $value
*/
public function __construct(array $value)
{
$this->value = $value;
}
}
2 changes: 1 addition & 1 deletion src/Annotation/Format.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
*
* @since 3.0.0
*/
#[Attribute(Attribute::TARGET_PROPERTY)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
final class Format
{

Expand Down
2 changes: 1 addition & 1 deletion src/Annotation/Relationship.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
*
* @psalm-suppress InvalidExtendClass
*/
#[Attribute(Attribute::TARGET_PROPERTY)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
final class Relationship extends Subtype
{
}
2 changes: 1 addition & 1 deletion src/Annotation/Subtype.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*
* @since 3.1.0
*/
#[Attribute(Attribute::TARGET_PROPERTY)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class Subtype
{

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,48 +11,52 @@

declare(strict_types=1);

namespace Sunrise\Hydrator;
namespace Sunrise\Hydrator\AnnotationReader;

use Generator;
use LogicException;
use ReflectionAttribute;
use ReflectionProperty;
use Sunrise\Hydrator\AnnotationReaderInterface;

use function sprintf;

use const PHP_MAJOR_VERSION;

/**
* @link https://www.php.net/attributes
*
* @since 3.1.0
*/
final class AnnotationReader implements AnnotationReaderInterface
final class BuiltinAnnotationReader implements AnnotationReaderInterface
{

/**
* Constructor of the class
*
* @throws LogicException If PHP version less than 8.0.
* @throws LogicException If the PHP version less than 8.0.
*/
public function __construct()
{
// @codeCoverageIgnoreStart
if (PHP_MAJOR_VERSION < 8) {
throw new LogicException(sprintf(
'The annotation reader {%s} requires PHP version greater than or equal to 8.0.',
__CLASS__,
));
}
} // @codeCoverageIgnoreEnd
}

/**
* @inheritDoc
*/
public function getAnnotations(ReflectionProperty $target, string $name): Generator
public function getAnnotations(string $name, $holder): Generator
{
// @codeCoverageIgnoreStart
if (PHP_MAJOR_VERSION < 8) {
return;
}
} // @codeCoverageIgnoreEnd

$attributes = $target->getAttributes($name, ReflectionAttribute::IS_INSTANCEOF);
$attributes = $holder->getAttributes($name, ReflectionAttribute::IS_INSTANCEOF);
foreach ($attributes as $attribute) {
yield $attribute->newInstance();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@

declare(strict_types=1);

namespace Sunrise\Hydrator;
namespace Sunrise\Hydrator\AnnotationReader;

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\Reader;
use Generator;
use LogicException;
use ReflectionProperty;
use Sunrise\Hydrator\AnnotationReaderInterface;

use function class_exists;
use function sprintf;
Expand Down Expand Up @@ -46,7 +47,7 @@ public function __construct(Reader $reader)
}

/**
* Creates the class instance with the doctrine's default annotation reader
* Creates a new instance of the class with the doctrine's default annotation reader
*
* @return self
*
Expand All @@ -57,7 +58,7 @@ public static function default(): self
// @codeCoverageIgnoreStart
if (!class_exists(AnnotationReader::class)) {
throw new LogicException(sprintf(
'The annotation reader {%s} requires the package doctrine/annotations, ' .
'The annotation reader {%s} requires the doctrine/annotations package, ' .
'run the command `composer require doctrine/annotations` to resolve it.',
__CLASS__,
));
Expand All @@ -69,12 +70,14 @@ public static function default(): self
/**
* @inheritDoc
*/
public function getAnnotations(ReflectionProperty $target, string $name): Generator
public function getAnnotations(string $name, $holder): Generator
{
$annotations = $this->reader->getPropertyAnnotations($target);
foreach ($annotations as $annotation) {
if ($annotation instanceof $name) {
yield $annotation;
if ($holder instanceof ReflectionProperty) {
$annotations = $this->reader->getPropertyAnnotations($holder);
foreach ($annotations as $annotation) {
if ($annotation instanceof $name) {
yield $annotation;
}
}
}
}
Expand Down
Loading

0 comments on commit 76dcd05

Please sign in to comment.