grifix/normalizer

此包已被废弃,不再维护。没有建议的替代包。

支持数据版本升级的Normalizer

维护者

详细信息

gitlab.com/grifix/normalizer

4.0 2022-11-11 06:48 UTC

This package is auto-updated.

Last update: 2023-07-26 07:06:40 UTC


README

支持升级的通用Normalizer。

描述

有时将实体存储在JSON中而不是为每个属性创建单独的字段非常方便,尤其是当你的实体中有许多值对象时。但当你想要重构实体并更改其内部结构时,问题就出现了。如果你使用传统的ORM,你应该编写一个迁移来更改你的表结构,如果你将数据存储为JSON,这可能会比较困难。这个库提供另一种解决方案。它不是创建迁移,而是建议创建一个版本转换器,在从仓库读取数据时实时更改数据结构。它控制你的实体结构,并在先前保存的数据结构与更改后的实体结构不兼容时警告你。

安装

composer require grifix/normalizer

Symfony集成

基本用法

假设我们想要标准化类 User

final class User{
    public function __construct(
        private readonly string $email,
        private readonly string $name
    ){

    }
}

我们应该为此类注册默认的normalizer

use Grifix\Normalizer\Normalizer;
use Grifix\Normalizer\SchemaValidator\Repository\Schema\Schema;

$normalizer = Normalizer::create();
$normalizer->registerDefaultObjectNormalizer(
    'user', 
    User::class, 
    [
        Schema::create()
        ->withStringProperty('email')
        ->withStringProperty('name'),
    ]
);

现在我们可以将 User 对象标准化为数组并将其存储在某个地方

$user = new User('user@example.com', 'John Smith');
print_r($normalizer->normalize($user));

将显示

Array
(
    [email] => john@example.com
    [name] => John Smith
    [__normalizer__] => Array
        (
            [name] => user
            [version] => 1
        )

)

我们还可以将数组反序列化为 User 对象

$user = $normalizer->denormalize([
    'email' => 'john@example.com',
    'name' => 'John Smith',
    '__normalizer__' => [
        'name' => 'user',
        'version' => 1
    ]
]);

升级

现在假设我们决定更改 User 对象的结构,并希望将 name 字段分开为 firstNamelastName

final class User{
    public function __construct(
        private readonly string $email,
        private readonly string $firstName,
        private readonly string $lastName
    ){

    }
}

如果我们尝试标准化这个新的 User

$user = new User('user@example.com', 'John', 'Smith');
print_r($normalizer->normalize($user));

对象normalizer将抛出异常

Invalid data for normalizer [user] version [1]! 

当然,我们可以直接修改JSON模式并解决标准化问题,但之前已标准化并保存的数据怎么办呢?

为了解决这个问题,我们需要做更多的努力:我们应该准备一个新的JSON模式版本,并准备一个转换器,将旧的数据结构转换为新的结构。


use Grifix\Normalizer\VersionConverter\Exceptions\UnsupportedVersionException;

$normalizer->registerDefaultObjectNormalizer(
    'user', 
    User::class, 
    [
        Schema::create()
            ->withStringProperty('email')
            ->withStringProperty('name'),
        Schema::create()
            ->withStringProperty('email')
            ->withStringProperty('firstName')
            ->withStringProperty('lastName'),
    ],
     new class implements \Grifix\Normalizer\VersionConverter\VersionConverterInterface {
        /**
         * @throws UnsupportedVersionException
         */
        public function convert(array $data, int $dataVersion, string $normalizerName): array
        {
            return match ($dataVersion) {
                1 => $this->convertToVersion2($data),
                default => throw new UnsupportedVersionException(
                    $normalizerName,
                    $dataVersion
                )
            };
        }

        private function convertToVersion2($data): array
        {
            $arr = explode(' ', $data['name']);
            $data['firstName'] = $arr[0];
            $data['lastName'] = $arr[1];
            unset($data['name']);
            return $data;
        }
    }
);

现在我们能够标准化新的 User 类版本

$user = new User('john@example.com', 'John', 'Smith');
print_r($normalizer->normalize($user));

将返回

Array
(
    [email] => john@example.com
    [firstName] => John
    [lastName] => Smith
    [__normalizer__] => Array
        (
            [name] => user
            [version] => 2
        )
)

我们还可以将旧版本的数据反序列化为对象

$normalizer->denormalize([
    'email' => 'john@example.com',
    'name' => 'John Smith',
    '__normalizer__' => [
        'name' => 'user',
        'version' => 1
    ]
]);

自定义normalizer

让我们更改我们的用户并添加出生日期

final class User
{
    public function __construct(
        private readonly string $email,
        private readonly string $firstName,
        private readonly string $lastName,
        private readonly ?DateTimeImmutable $birthDate = null
    ) {
    }
}

我们还应该修改注册用户normalizer的代码,并为新版本添加json模式

Schema::create()
    ->withStringProperty('email')
    ->withStringProperty('firstName')
    ->withStringProperty('lastName')
    ->withObjectProperty('birthDate', ['date-time'], true)

并修改版本转换器

new class implements \Grifix\Normalizer\VersionConverter\VersionConverterInterface {
    /**
     * @throws UnsupportedVersionException
     */
    public function convert(array $data, int $dataVersion, string $normalizerName): array
    {
        return match ($dataVersion) {
            1 => $this->convertToVersion2($data),
            2 => $this->convertToVersion3($data),
            default => throw new UnsupportedVersionException(
                $normalizerName,
                $dataVersion
            )
        };
    }

    private function convertToVersion2($data): array
    {
        $arr = explode(' ', $data['name']);
        $data['firstName'] = $arr[0];
        $data['lastName'] = $arr[1];
        unset($data['name']);
        return $data;
    }

    private function convertToVersion3($data): array
    {
        $data['birthDate'] = null;
        return $data;
    }
}

现在它对旧数据格式也有效

$normalizer->denormalize([
    'email' => 'john@example.com',
    'name' => 'John Smith',
    '__normalizer__' => [
        'name' => 'user',
        'version' => 1
    ]
]);

$normalizer->denormalize([
    'email' => 'john@example.com',
    'firstName' => 'John',
    'lastName' => 'Smith',
    '__normalizer__' => [
        'name' => 'user',
        'version' => 2
    ]
]);

但如果我们尝试标准化具有出生日期的用户

$normalizer->normalize(
    new User(
        'john@example.com',
        'John',
        'Smith',
        new DateTimeImmutable()
    )
);

我们将得到一个异常

Normalizer with object class [DateTimeImmutable] does not exist! 

因此,我们可以尝试注册默认的 DateTimeImmutable 类normalizer

$normalizer->registerDefaultObjectNormalizer(
    'DateTimeImmutable', 
    DateTimeImmutable::class,
    [
        Schema::create()->withStringProperty('value'),       
    ]   
);

如果我们再次尝试标准化我们的用户

print_r($normalizer->normalize(
    new User(
        'john@example.com',
        'John',
        'Smith',
        new DateTimeImmutable()
    )
));

我们将看到如下内容

Array
(
    [email] => john@example.com
    [firstName] => John
    [lastName] => Smith
    [birthDate] => Array
        (
            [__normalizer__] => Array
                (
                    [name] => DateTimeImmutable
                    [version] => 1
                )

        )

    [__normalizer__] => Array
        (
            [name] => user
            [version] => 3
        )

)

如我们所见,数据数组中没有关于出生日期的信息。这是因为默认normalizer无法处理内置对象或某些库对象,因为它们可能包含一些额外的对象(如服务)或我们不想存储的数据。

在这种情况下,我们应该注册一个自定义normalizer,描述如何标准化和反序列化此类对象

$normalizer->registerCustomObjectNormalizer(
    'date-time',
    new class implements \Grifix\Normalizer\ObjectNormalizers\CustomObjectNormalizerInterface{

        public function normalize(object $object): array
        {
            if ( ! ($object instanceof \DateTimeImmutable)) {
                throw new \Grifix\Normalizer\ObjectNormalizers\Exceptions\InvalidObjectTypeException(
                    $object::class, 
                    \DateTimeImmutable::class
                );
            }

            return ['value' => $object->format(DateTimeInterface::ATOM)];
        }

        public function denormalize(array $data): object
        {
            return new \DateTimeImmutable($data['value']);
        }

        public function getObjectClass(): string
        {
            return \DateTimeImmutable::class;
        }
    },
    [
        Schema::create()->withStringProperty('value');
    ]
);

现在我们的输出将如下所示

Array
(
    [email] => john@example.com
    [firstName] => John
    [lastName] => Smith
    [birthDate] => Array
        (
            [value] => 2022-06-22T20:00:03+00:00
            [__normalizer__] => Array
                (
                    [name] => DateTimeImmutable
                    [version] => 1
                )

        )

    [__normalizer__] => Array
        (
            [name] => user
            [version] => 3
        )

)

依赖注入

现在假设我们需要在我们的 User 对象中包含一些服务,我们不想存储它,但希望在用户反序列化后自动注入

final class PrinterService
{
    public function print(string $value): void
    {
        echo $value.PHP_EOL;
    }
}

final class User
{
    public function __construct(
        private readonly string $email,
        private readonly string $firstName,
        private readonly string $lastName,
        private readonly ?DateTimeImmutable $birthDate = null,
        private readonly PrinterService $printer
    ) {
        $this->printer->print($this->email);
    }

    public function printName():void{
        $this->printer->print($this->firstName.' '.$this->lastName);
    }
}

现在我们应该告诉normalizer,printer 属性是一个依赖项,应该自动注入

$normalizer->registerDefaultObjectNormalizer(
    'user', 
    User::class, 
    [
        //schemas      
    ], 
    //converter,
    ['printer']
);

并且我们应该将打印机服务注册为依赖项

$printer = new PrinterService();
$normalizer->registerDependency($printer);

在实际应用中,您可以通过包装IOC容器来实现\Grifix\Normalizer\DependencyProvider\DependencyProviderInterface

如果我们现在执行此代码

$user = new User(
    'john@example.com',
    'John',
    'Smith',
    new DateTimeImmutable(),
    $printer
);

$data = $normalizer->normalize($user);
$user = $normalizer->denormalize($data);
$user->printName();

输出将是

john@example.com
John Smith

因为正常化器在反序列化时没有调用构造函数。