patchlevel/hydrator

1.5.0 2024-09-18 12:04 UTC

README

Mutation testing badge Type Coverage Latest Stable Version License

Hydrator

使用这个库,您可以将数组中的对象转换为对象,并再次将对象转换回数组,重点关注从数据库到数据库的数据处理。现在,它已作为event-sourcing库的独立库外包。

安装

composer require patchlevel/hydrator

用法

要使用hydrator,只需创建其实例。

use Patchlevel\Hydrator\MetadataHydrator;

$hydrator = new MetadataHydrator();

之后,您可以为任何类或对象进行填充。还可以使用属性提升对finalreadonly类进行填充。

final readonly class ProfileCreated 
{
    public function __construct(
        public string $id,
        public string $name
    ) {
    }
}

提取数据

要将对象转换为可序列化的数组,您可以使用extract方法

$event = new ProfileCreated('1', 'patchlevel');

$data = $hydrator->extract($event);
[
  'id' => '1',
  'name' => 'patchlevel'
]

填充对象

$event = $hydrator->hydrate(
    ProfileCreated::class,
    [
        'id' => '1',
        'name' => 'patchlevel'
    ]
);

$oldEvent == $event // true

规范器

有时您还需要提取或填充更复杂的对象。为此,您可以使用规范器。对于一些标准情况,我们已经提供了内置规范器。

数组

如果您有一个要规范化的对象列表,则必须单独规范化每个对象。这正是ArrayNormalizer为您所做的事情。为了使用ArrayNormaliser,您仍然需要指定应应用于单个对象的规范器。内部,它基本上执行了一个array_map,然后对每个元素运行指定的规范器。

use Patchlevel\Hydrator\Normalizer\ArrayNormalizer;
use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer;

final class DTO 
{
    #[ArrayNormalizer(new DateTimeImmutableNormalizer())]
    public array $dates;
}

注意

这里采用了数组的键。

DateTimeImmutable

如名称所示,使用DateTimeImmutable规范器,您可以转换DateTimeImmutable对象并将其转换回字符串。

use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer;

final class DTO 
{
    #[DateTimeImmutableNormalizer]
    public DateTimeImmutable $date;
}

您也可以定义格式。您可以将其描述为字符串或使用现有的常量之一。默认为DateTimeImmutable::ATOM

use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer;

final class DTO 
{
    #[DateTimeImmutableNormalizer(format: DateTimeImmutable::RFC3339_EXTENDED)]
    public DateTimeImmutable $date;
}

注意

您可以在php 文档中了解格式的结构。

DateTime

DateTime规范器的工作方式与DateTime规范器完全相同。仅适用于DateTime对象。

use Patchlevel\Hydrator\Normalizer\DateTimeNormalizer;

final class DTO 
{
    #[DateTimeNormalizer]
    public DateTime $date;
}

您也可以在这里指定格式。默认为DateTime::ATOM

use Patchlevel\Hydrator\Normalizer\DateTimeNormalizer;

final class DTO 
{
    #[DateTimeNormalizer(format: DateTime::RFC3339_EXTENDED)]
    public DateTime $date;
}

注意

您可以在php 文档中了解格式的结构。

DateTimeZone

要规范化DateTimeZone,可以使用DateTimeZoneNormalizer

use Patchlevel\Hydrator\Normalizer\DateTimeZoneNormalizer;

final class DTO
{
    #[DateTimeZoneNormalizer]
    public DateTimeZone $timeZone;
}

枚举

受支持的枚举也可以进行规范化。为此,必须传递枚举FQCN,以便EnumNormalizer知道它是哪个枚举。

use Patchlevel\Hydrator\Normalizer\EnumNormalizer;

final class DTO
{
    #[EnumNormalizer]
    public Status $status;
}

对象

如果您有一个要规范化的复杂对象,则可以使用ObjectNormalizer。这使用hydrator内部进行对象规范化。

use Patchlevel\Hydrator\Normalizer\ObjectNormalizer;

final class DTO
{
    #[ObjectNormalizer]
    public AnohterDto $anotherDto;
    
    #[ObjectNormalizer(AnohterDto::class)]
    public object $object;
}

final class AnotherDto
{
    #[EnumNormalizer]
    public Status $status;
}

警告

不支持循环引用,这会导致异常。

自定义规范器

由于我们只提供PHP原生事物的规范器,因此您必须为您自己的结构(如值对象)编写自己的规范器。

在我们的示例中,我们构建了一个应包含名称的值对象。

final class Name
{
    private string $value;
    
    public function __construct(string $value) 
    {
        if (strlen($value) < 3) {
            throw new NameIsToShortException($value);
        }
        
        $this->value = $value;
    }
    
    public function toString(): string 
    {
        return $this->value;
    }
}

为此,我们现在需要一个自定义规范器。此规范器必须实现Normalizer接口。您还需要实现normalizedenormalize方法。最后,您必须允许规范器作为属性使用。

use Patchlevel\Hydrator\Normalizer\Normalizer;
use Patchlevel\Hydrator\Normalizer\InvalidArgument;

#[Attribute(Attribute::TARGET_PROPERTY)]
class NameNormalizer implements Normalizer
{
    public function normalize(mixed $value): string
    {
        if (!$value instanceof Name) {
            throw InvalidArgument::withWrongType(Name::class, $value);
        }

        return $value->toString();
    }

    public function denormalize(mixed $value): ?Name
    {
        if ($value === null) {
            return null;
        }

        if (!is_string($value)) {
            throw InvalidArgument::withWrongType('string', $value);
        }

        return new Name($value);
    }
}

警告

重要的是,规范化操作的结果必须是可序列化的!

现在我们也可以直接使用规范器。

final class DTO
{
    #[NameNormalizer]
    public Name $name
}

在类级别上定义规范器

您还可以在类级别上设置值对象的属性。为此,规范化器需要允许在类级别上设置。

use Patchlevel\Hydrator\Normalizer\Normalizer;
use Patchlevel\Hydrator\Normalizer\InvalidArgument;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS)]
class NameNormalizer implements Normalizer
{
    // ... same as before
}

然后设置值对象的属性。

#[NameNormalizer]
final class Name
{
    // ... same as before
}

之后,DTO可以看起来像这样。

final class DTO
{
    public Name $name
}

推断规范化器

我们还集成了一个过程,其中规范化器通过类型进行推断。这意味着您不需要在属性或类级别上定义规范化器。目前这仅适用于我们库定义的规范化器。但是有一些例外,即ObjectNormalizerArrayNormalizer

这些规范化器可以被推断

  • DateTimeImmutableNormalization
  • DateTimeNormalization
  • DateTimeZoneNormalization
  • EnumNormalization

标准化名称

默认情况下,属性名称用于在规范化结果中命名字段。这可以通过NormalizedName属性进行自定义。

use Patchlevel\Hydrator\Attribute\NormalizedName;

final class DTO
{
    #[NormalizedName('profile_name')]
    public string $name
}

整个结构看起来像这样

[
  'profile_name' => 'David'
]

提示

您还可以通过保持序列化名称来重命名属性为事件,而不会破坏向后兼容性。

忽略

有时需要排除属性。您可以使用Ignore属性来做到这一点。属性在提取和填充时都会被忽略。

use Patchlevel\Hydrator\Attribute\Ignore;

readonly class ProfileCreated 
{
    public function __construct(
        public string $id,
        public string $name,
        #[Ignore]
        public string $ignoreMe,
    ) {
    }
}

钩子

有时需要在提取或填充过程之前或之后执行某些操作。为此,我们有PreExtractPostHydrate属性。

use Patchlevel\Hydrator\Attribute\PostHydrate;
use Patchlevel\Hydrator\Attribute\PreExtract;

readonly class Dto 
{    
    #[PostHydrate]
    private function postHydrate(): void
    {
        // do something
    }
    
    #[PreExtract]
    private function preExtract(): void
    {
        // do something
    }
}

密码学

库还提供了加密和解密个人数据的功能。

个人数据

首先,我们必须标记包含个人数据的字段。在我们的示例中,我们使用事件,但您也可以使用聚合体。

use Patchlevel\Hydrator\Attribute\PersonalData;

final class DTO 
{
    #[PersonalData]
    public readonly string|null $email;
}

如果信息无法解密,则插入回退值。默认回退值是null。您可以通过设置fallback参数来更改此值。在这种情况下,添加了unknown

use Patchlevel\Hydrator\Attribute\PersonalData;

final class DTO
{
    public function __construct(
        #[PersonalData(fallback: 'unknown')]
        public readonly string $email,
    ) {
    }
}

[!DANGER]您必须在您的业务逻辑中处理此情况,例如聚合体和订阅。

警告

您需要定义一个主题ID以使用个人数据属性。

数据主体ID

为了使用正确的密钥,必须定义主题ID。没有主题ID,就无法加密或解密个人数据。

use Patchlevel\Hydrator\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Attribute\PersonalData;

final class EmailChanged
{
    public function __construct(
        #[DataSubjectId]
        public readonly string $personId,
        #[PersonalData(fallback: 'unknown')]
        public readonly string|null $email,
    ) {
    }
}

警告

主题ID不能是个人数据。

配置密码学

在这里,我们向您展示如何配置密码学。

use Patchlevel\Hydrator\Cryptography\PersonalDataPayloadCryptographer;
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyStore;
use Patchlevel\Hydrator\Metadata\Event\EventMetadataFactory;
use Patchlevel\Hydrator\MetadataHydrator;

$cipherKeyStore = new InMemoryCipherKeyStore();
$cryptographer = PersonalDataPayloadCryptographer::createWithOpenssl($cipherKeyStore);
$hydrator = new MetadataHydrator(cryptographer: $cryptographer);

密钥存储库

密钥必须存储在某个地方。出于测试目的,我们提供了一个内存实现。

use Patchlevel\Hydrator\Cryptography\Cipher\CipherKey;
use Patchlevel\Hydrator\Cryptography\Store\InMemoryCipherKeyStore;

$cipherKeyStore = new InMemoryCipherKeyStore();

/** @var CipherKey $cipherKey */
$cipherKeyStore->store('foo-id', $cipherKey);
$cipherKey = $cipherKeyStore->get('foo-id');
$cipherKeyStore->remove('foo-id');

因为我们不知道您想在何处存储密钥,所以我们没有提供任何其他实现。您应该使用数据库或密钥存储来做到这一点。为此,您必须实现CipherKeyStore接口。

删除个人数据

要删除个人数据,您只需要从存储库中删除密钥。

$cipherKeyStore->remove('foo-id');