Skip to content

Commit

Permalink
Merge pull request #30 from sunrise-php/release/v3.3.0
Browse files Browse the repository at this point in the history
v3.3.0
  • Loading branch information
fenric authored Oct 11, 2023
2 parents 76dcd05 + 36cf674 commit 9f35d8b
Show file tree
Hide file tree
Showing 98 changed files with 1,932 additions and 1,988 deletions.
105 changes: 74 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Strongly typed hydrator for PHP 7.4+ with support for PHP 8.1 enums
# Strongly typed hydrator for PHP 7.4+ with extensibility

[![Build Status](https://scrutinizer-ci.com/g/sunrise-php/hydrator/badges/build.png?b=main)](https://scrutinizer-ci.com/g/sunrise-php/hydrator/build-status/main)
[![Code Coverage](https://scrutinizer-ci.com/g/sunrise-php/hydrator/badges/coverage.png?b=main)](https://scrutinizer-ci.com/g/sunrise-php/hydrator/?branch=main)
Expand Down Expand Up @@ -30,8 +30,8 @@ composer require sunrise/hydrator
* * [Array](#array)
* * [Timestamp](#timestamp)
* * [Timezone](#timezone)
* * [UID](#uid)
* * [Enumeration](#enumeration)
* * [UUID](#uid)
* * [Relationship](#relationship)
* * [Custom type](#support-for-custom-types)
* [Ignored property](#ignored-property)
Expand All @@ -49,33 +49,47 @@ enum Status: int
case ENABLED = 1;
case DISABLED = 0;
}
```

final class Category
```php
final class CategoryDto
{
public function __construct(
public readonly string $name,
) {
}
}
```

final class Tag
```php
final class TagDto
{
public function __construct(
public readonly string $name,
) {
}
}
```

final class Product
```php
final class TagDtoCollection implements ArrayAccess
{
public function __construct(TagDto ...$tags)
{
}
}
```

```php
final class PublicationDto
{
public function __construct(
public readonly string $name,
public readonly Category $category,
#[\Sunrise\Hydrator\Annotation\Subtype(Tag::class, limit: 100)]
public readonly array $tags,
public readonly CategoryDto $category,
public readonly TagDtoCollection $tags,
public readonly Status $status = Status::DISABLED,
#[\Sunrise\Hydrator\Annotation\Format(\DATE_RFC3339)]
public readonly DateTimeImmutable $createdAt = new DateTimeImmutable(),
#[\Sunrise\Hydrator\Annotation\Format(DateTimeInterface::RFC3339)]
public readonly DateTimeImmutable $createdAt = new DateTimeImmutable('now'),
) {
}
}
Expand All @@ -100,7 +114,7 @@ $data = [
'status' => 0,
];

$product = (new \Sunrise\Hydrator\Hydrator)->hydrate(Product::class, $data);
$product = (new \Sunrise\Hydrator\Hydrator)->hydrate(PublicationDto::class, $data);
```

Or, you can populate them using JSON:
Expand All @@ -124,7 +138,7 @@ $json = <<<JSON
}
JSON;

$product = (new \Sunrise\Hydrator\Hydrator)->hydrateWithJson(Product::class, $json);
$product = (new \Sunrise\Hydrator\Hydrator)->hydrateWithJson(PublicationDto::class, $json);
```

## Allowed property types
Expand Down Expand Up @@ -232,7 +246,7 @@ final class TagDto
```

```php
final class TagCollection implements \ArrayAccess
final class TagDtoCollection implements \ArrayAccess
{
}
```
Expand All @@ -242,7 +256,7 @@ final class CreateProductDto
{
public function __construct(
#[\Sunrise\Hydrator\Annotation\Subtype(TagDto::class, limit: 10)]
public readonly TagCollection $tags,
public readonly TagDtoCollection $tags,
) {
}
}
Expand All @@ -253,7 +267,7 @@ Note that for collections, instead of the **Subtype** annotation, you can use ty
> 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
final class TagCollection implements \ArrayAccess
final class TagDtoCollection implements \ArrayAccess
{
public function __construct(public TagDto ...$tags)
{
Expand All @@ -265,7 +279,7 @@ final class TagCollection implements \ArrayAccess
final class CreateProductDto
{
public function __construct(
public readonly TagCollection $tags,
public readonly TagDtoCollection $tags,
) {
}
}
Expand All @@ -283,7 +297,7 @@ This property has no any additional behavior and only accepts arrays.

### Timestamp

Only the DateTimeImmutable type is supported.
Only the [DateTimeImmutable](https://www.php.net/DateTimeImmutable) type is supported.

```php
#[\Sunrise\Hydrator\Annotation\Format('Y-m-d H:i:s')]
Expand Down Expand Up @@ -312,7 +326,7 @@ $hydrator = new Hydrator([

### Timezone

Only the DateTimeZone type is supported.
Only the [DateTimeZone](https://www.php.net/DateTimeZone) type is supported.

```php
public readonly DateTimeZone $value;
Expand All @@ -331,7 +345,19 @@ $hydrator = new Hydrator([
]);
```

### UID
### UUID

#### Using the [ramsey/uuid](https://github.com/ramsey/uuid) package

```bash
composer require ramsey/uuid
```

```php
public readonly \Ramsey\Uuid\UuidInterface $value;
```

#### Using the [symfony/uid](https://github.com/symfony/uid) package

```bash
composer require symfony/uid
Expand All @@ -345,14 +371,27 @@ Also, please note that if a value in a dataset for this property is represented

### Enumeration

#### PHP 8.1 [built-in](https://www.php.net/enum) enumerations

```php
public readonly SomeEnum $value;
```

This property should be typed only with typed enumerations. Therefore, for integer enumerations, a value in a dataset can be either an integer or an integer represented as a string. For string enumerations, a value in a dataset can only be a string.

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).
#### [MyCLabs](https://github.com/myclabs/php-enum) enumerations

_The popular alternative for PHP less than 8.1..._

```bash
composer require myclabs/php-enum
```

```php
public readonly SomeEnum $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).

### Relationship

Expand All @@ -364,49 +403,53 @@ A value in a dataset can only be an array. However, please note that if you need

## 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:
If you need support for a custom type, it is a relatively simple task. Let's write such support for PSR-7 URI from the [sunrise/http-message](https://github.com/sunrise-php/http-message) package:

```bash
composer require sunrise/http-message
```

```php
use Sunrise\Hydrator\Exception\InvalidValueException;
use Sunrise\Hydrator\Type;
use Sunrise\Hydrator\TypeConverterInterface;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use Psr\Message\UriInterface;
use Sunrise\Http\Message\Uri;

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

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

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

yield Uuid::fromString($value);
}

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

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

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

## Ignored property
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"php": ">=7.4"
},
"require-dev": {
"sunrise/coding-standard": "~1.0.0",
"sunrise/coding-standard": "^1.0",
"phpunit/phpunit": "^9.6",
"vimeo/psalm": "^5.15",
"phpstan/phpstan": "^1.10",
Expand Down
1 change: 1 addition & 0 deletions src/Annotation/Relationship.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
*
* @Attributes({
* @Attribute("name", type="string", required=true),
* @Attribute("allowsNull", type="boolean", required=false),
* @Attribute("limit", type="integer", required=false),
* })
*
Expand Down
12 changes: 11 additions & 1 deletion src/Annotation/Subtype.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
*
* @Attributes({
* @Attribute("name", type="string", required=true),
* @Attribute("allowsNull", type="boolean", required=false),
* @Attribute("limit", type="integer", required=false),
* })
*
Expand All @@ -40,6 +41,13 @@ class Subtype
*/
public string $name;

/**
* @var bool
*
* @readonly
*/
public bool $allowsNull;

/**
* @var int<0, max>|null
*
Expand All @@ -51,11 +59,13 @@ class Subtype
* Constructor of the class
*
* @param non-empty-string $name
* @param bool $allowsNull
* @param int<0, max>|null $limit
*/
public function __construct(string $name, ?int $limit = null)
public function __construct(string $name, bool $allowsNull = false, ?int $limit = null)
{
$this->name = $name;
$this->allowsNull = $allowsNull;
$this->limit = $limit;
}
}
2 changes: 1 addition & 1 deletion src/Exception/InvalidObjectException.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ final public static function unsupportedMethodParameterType(Type $type, Reflecti
final public static function unsupportedFunctionParameterType(Type $type, ReflectionParameter $parameter, ReflectionFunctionAbstract $function): self
{
return new self(sprintf(
'The parameter {%s($%s[%d])} is associated with the unsupported type {%s}.',
'The parameter {%s($%s[%d])} is associated with an unsupported type {%s}.',
$function->getName(),
$parameter->getName(),
$parameter->getPosition(),
Expand Down
2 changes: 1 addition & 1 deletion src/Hydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ private static function defaultTypeConverters(): Generator
if (class_exists(\MyCLabs\Enum\Enum::class)) {
yield new MyclabsEnumTypeConverter();
}
if (class_exists(\Ramsey\Uuid\UuidInterface::class)) {
if (class_exists(\Ramsey\Uuid\Uuid::class)) {
yield new RamseyUuidTypeConverter();
}
if (class_exists(\Symfony\Component\Uid\AbstractUid::class)) {
Expand Down
23 changes: 14 additions & 9 deletions src/TypeConverter/ArrayAccessTypeConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use Sunrise\Hydrator\TypeConverterInterface;

use function count;
use function end;
use function is_array;
use function is_subclass_of;

Expand Down Expand Up @@ -97,9 +98,8 @@ public function castValue($value, Type $type, array $path, array $context): Gene
throw InvalidValueException::mustBeArray($path);
}

$subtype = $this->annotationReader->getAnnotations(Subtype::class, $type->getHolder())->current();

$subtype ??= $this->getContainerSubtype($containerReflection);
// phpcs:ignore Generic.Files.LineLength
$subtype = $this->annotationReader->getAnnotations(Subtype::class, $type->getHolder())->current() ?? $this->getContainerSubtype($containerReflection);

if ($subtype === null) {
$counter = 0;
Expand All @@ -125,7 +125,7 @@ public function castValue($value, Type $type, array $path, array $context): Gene
try {
$container[$key] = $this->hydrator->castValue(
$element,
new Type($type->getHolder(), $subtype->name, false),
new Type($type->getHolder(), $subtype->name, $subtype->allowsNull),
[...$path, $key],
$context,
);
Expand Down Expand Up @@ -162,6 +162,8 @@ public function getWeight(): int
* @param ReflectionClass $container
*
* @return Subtype|null
*
* @codeCoverageIgnore
*/
private function getContainerSubtype(ReflectionClass $container): ?Subtype
{
Expand All @@ -171,20 +173,23 @@ private function getContainerSubtype(ReflectionClass $container): ?Subtype
}

$parameters = $constructor->getParameters();
if (count($parameters) <> 1) {
if ($parameters === []) {
return null;
}

$parameter = $parameters[0];
if (!$parameter->isVariadic()) {
$parameter = end($parameters);
if ($parameter->isVariadic() === false) {
return null;
}

$type = $parameter->getType();
if (! $type instanceof ReflectionNamedType) {
if ($type === null) {
return null;
}

return new Subtype($type->getName());
/** @var non-empty-string $name */
$name = ($type instanceof ReflectionNamedType) ? $type->getName() : (string) $type;

return new Subtype($name, $type->allowsNull());
}
}
Loading

0 comments on commit 9f35d8b

Please sign in to comment.