sunrise/hydrator

PHP 7.4+对象水化器


README

Build Status Code Coverage Scrutinizer Code Quality Total Downloads Latest Stable Version License

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

安装

composer require sunrise/hydrator

导航

如何使用

让我们考虑一个典型的DTO集合

enum Status: int
{
    case ENABLED = 1;
    case DISABLED = 0;
}
final class CategoryDto
{
    public function __construct(
        public readonly string $name,
    ) {
    }
}
final class TagDto
{
    public function __construct(
        public readonly string $name,
    ) {
    }
}
final class TagDtoCollection implements ArrayAccess
{
    public function __construct(TagDto ...$tags)
    {
    }
}
final class PublicationDto
{
    public function __construct(
        public readonly string $name,
        public readonly CategoryDto $category,
        public readonly TagDtoCollection $tags,
        public readonly Status $status = Status::DISABLED,
        #[\Sunrise\Hydrator\Annotation\Format(DateTimeInterface::RFC3339)]
        public readonly DateTimeImmutable $createdAt = new DateTimeImmutable('now'),
    ) {
    }
}

现在,让我们从数组中填充它们

$data = [
    'name' => 'Some product',
    'category' => [
        'name' => 'Some category',
    ],
    'tags' => [
        [
            'name' => 'foo',
        ],
        [
            'name' => 'bar',
        ],
    ],
    'status' => 0,
];

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

或者,您可以使用JSON来填充它们

$json = <<<JSON
{
    "name": "Some product",
    "category": {
        "name": "Some category"
    },
    "tags": [
        {
            "name": "foo"
        },
        {
            "name": "bar"
        }
    ],
    "status": 0
}
JSON;

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

允许的属性类型

必需

public readonly string $value;

如果属性没有默认值,则该属性为必需。

可选

public readonly string $value = 'foo';

如果属性有默认值,则该属性为可选。

空值

public readonly ?string $value;

如果属性是可空的,则该属性可以接受空值。

此外,请注意,对于某些属性,空字符串或只包含空白的字符串将被处理为空值。

布尔值

public readonly bool $value;

数据集中布尔值可以表示为

此外,请注意,如果数据集中该属性的值为空字符串或只包含空白的字符串,则该值将被处理为空值

整数

public readonly int $value;

数据集中整数值也可以表示为字符串,例如,“42”。

此外,请注意,如果数据集中该属性的值为空字符串或只包含空白的字符串,则该值将被处理为空值

数字

public readonly float $value;

数据集中数值不仅可以表示为整数或浮点数,还可以表示为包含数字的字符串。但是,无论此类值在数据集中的表示方式如何,它都将始终以浮点数的形式存储在此属性中。

此外,请注意,如果数据集中该属性的值为空字符串或只包含空白的字符串,则该值将被处理为空值

字符串

public readonly string $value;

此属性没有任何额外的行为,仅接受字符串。

数组

public readonly array $value;

默认情况下,此属性接受任何数据的数组。但是,您也可以使用特殊注解来存储关系,如下例所示

#[\Sunrise\Hydrator\Annotation\Subtype(SomeDto::class)]
public readonly array $value;

换句话说,Subtype注解可以包含与类属性相同的数据类型...

在数组中有无限数量的关系可能是一个不好的主意,因为它可能导致内存泄漏。为了避免这种情况,建议限制此类数组,如下例所示

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

除了数组之外,您还可以使用集合,换句话说,实现ArrayAccess接口的类,例如

final class TagDto
{
}
final class TagDtoCollection implements \ArrayAccess
{
}
final class CreateProductDto
{
    public function __construct(
        #[\Sunrise\Hydrator\Annotation\Subtype(TagDto::class, limit: 10)]
        public readonly TagDtoCollection $tags,
    ) {
    }
}

请注意,对于集合,您可以使用其构造函数通过类型进行注解,而不是使用Subtype注解。重要的是其中只有一个可变参数。请参阅以下示例

请注意,在这种情况下,您需要负责限制集合。为了确保水化器了解集合何时已满,应使用offsetSet方法抛出OverflowException

final class TagDtoCollection implements \ArrayAccess
{
    public function __construct(public TagDto ...$tags)
    {
    }
}
final class CreateProductDto
{
    public function __construct(
        public readonly TagDtoCollection $tags,
    ) {
    }
}

通常,请记住,无论使用数组还是集合,其元素都可以进行类型注解。例如,如果您需要一个仅由日期组成的数组,则您的代码应如下所示

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

有时,需要将类型化列表映射到集合中。这是一个相对简单的任务;只需使用下面的示例即可。

use Sunrise\Hydrator\Type;

$data = [...];

$collection = $hydrator->castValue($data, Type::fromName(SomeDtoCollection::class));

该属性没有任何额外的行为,只接受数组。

时间戳

仅支持DateTimeImmutable类型。

#[\Sunrise\Hydrator\Annotation\Format('Y-m-d H:i:s')]
public readonly DateTimeImmutable $value;

该属性接受指定格式的日期字符串,也可以接受整数或字符串形式的Unix时间戳。要指定Unix时间戳格式,应如下所示

#[\Sunrise\Hydrator\Annotation\Format('U')]
public readonly DateTimeImmutable $value;

此外,请注意,如果数据集中该属性的值为空字符串或只包含空白的字符串,则该值将被处理为空值

默认时间戳格式

use Sunrise\Hydrator\Dictionary\ContextKey;
use Sunrise\Hydrator\Hydrator;

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

时区

仅支持DateTimeZone类型。

public readonly DateTimeZone $value;

此外,请注意,如果数据集中该属性的值为空字符串或只包含空白的字符串,则该值将被处理为空值

默认时区

use Sunrise\Hydrator\Dictionary\ContextKey;
use Sunrise\Hydrator\Hydrator;

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

UUID

使用ramsey/uuid

composer require ramsey/uuid
public readonly \Ramsey\Uuid\UuidInterface $value;

使用symfony/uid

composer require symfony/uid
public readonly \Symfony\Component\Uid\UuidV4 $value;

此外,请注意,如果数据集中该属性的值为空字符串或只包含空白的字符串,则该值将被处理为空值

枚举

PHP 8.1内置枚举

public readonly SomeEnum $value;

该属性只能使用类型化枚举进行类型化。因此,对于整数枚举,数据集中的值可以是整数或表示为字符串的整数。对于字符串枚举,数据集中的值只能为字符串。

MyCLabs枚举

PHP小于8.1的流行替代品...

composer require myclabs/php-enum
public readonly SomeEnum $value;

此外,请注意,如果数据集中该属性的值为空字符串或只包含空白的字符串,则该值将被处理为空值

关系

public readonly SomeDto $value;

数据集中的值只能为数组。但是请注意,如果您需要一个一对一或多对一的关系,您应该参考数组部分以获取更多信息。

支持自定义类型

如果您需要支持自定义类型,这是一个相对简单的任务。让我们从sunrise/http-message包中为PSR-7 URI编写这样的支持。

composer require sunrise/http-message
use Sunrise\Hydrator\Exception\InvalidValueException;
use Sunrise\Hydrator\Type;
use Sunrise\Hydrator\TypeConverterInterface;
use Psr\Message\UriInterface;
use Sunrise\Http\Message\Uri;

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

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

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

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

现在,让我们通知填充器关于新类型的信息

$hydrator->addTypeConverter(new UriTypeConverter());

忽略属性

如果您需要在对象填充过程中忽略某个属性且不对其进行填充,请使用以下示例中的特殊注解

#[\Sunrise\Hydrator\Annotation\Ignore]
public string $value;

属性别名

如果您需要在数据集中处理未规范的键或在其他原因下将此类键与具有不同名称的属性相关联,您可以为此目的使用特殊注解

#[\Sunrise\Hydrator\Annotation\Alias('error-codes')]
public array $errorCodes = [];

错误处理

try {
    $hydrator->hydrate(...);
} catch (\Sunrise\Hydrator\Exception\InvalidDataException $e) {
    // It's runtime error
} catch (\Sunrise\Hydrator\Exception\InvalidObjectException $e) {
    // It's logic error
}

InvalidDataException异常包含与输入数据集相关的错误,并设计用于在客户端直接显示错误。

如果您正在使用symfony/validator包,您可能会发现将错误作为\Symfony\Component\Validator\ConstraintViolationListInterface显示很有用。为此,您可以在该异常上调用以下方法

try {
    $hydrator->hydrate(...);
} catch (\Sunrise\Hydrator\Exception\InvalidDataException $e) {
    $violations = $e->getViolations();
}

或者,您也可以像以下示例中那样以标准方式检索错误列表

try {
    $hydrator->hydrate(...);
} catch (\Sunrise\Hydrator\Exception\InvalidDataException $e) {
    $errors = $e->getExceptions();
    foreach ($errors as $error) {
        echo $error->getMessage(), PHP_EOL;
        echo $error->getPropertyPath(), PHP_EOL;
        echo $error->getErrorCode(), PHP_EOL;
    }
}

本地化

$localizedMessages = [
    // using the error code dictionary...
    \Sunrise\Hydrator\Dictionary\ErrorCode::INVALID_TIMESTAMP => 'Это значение не является валидной временной меткой; ожидаемый формат: {{ expected_format }}.',

    // or using the error message dictionary...
    \Sunrise\Hydrator\Dictionary\ErrorMessage::INVALID_TIMESTAMP => 'Это значение не является валидной временной меткой; ожидаемый формат: {{ expected_format }}.',
];

try {
    $hydrator->hydrate(...);
} catch (\Sunrise\Hydrator\Exception\InvalidDataException $e) {
    foreach ($e->getExceptions() as $error) {
        // original message...
        $message = $error->getMessage();

        // using the error code dictionary...
        if (isset($localizedMessages[$error->getErrorCode()])) {
            // localized message
            $message = \strtr($localizedMessages[$error->getErrorCode()], $error->getMessagePlaceholders()), PHP_EOL;
        }

        // or using the error message dictionary...
        if (isset($localizedMessages[$error->getMessageTemplate()])) {
            // localized message
            $message = \strtr($localizedMessages[$error->getMessageTemplate()], $error->getMessagePlaceholders()), PHP_EOL;
        }
    }
}

Doctrine注解

要使用注解,您需要安装doctrine/annotations

composer require doctrine/annotations

要使用PHP 7或显式在PHP 8中使用注解,您可以使用填充器的以下方法

$hydrator->useDefaultAnnotationReader();

如果您需要提供自己的注解读取器实例,您可以使用以下方法

$hydrator->setAnnotationReader(...);

测试运行

composer test