grifix / normalizer
支持数据版本升级的Normalizer
Requires
- php: ^8.1
- grifix/array-wrapper: ^2.0
- grifix/reflection: ^1.0
- justinrainbow/json-schema: ^5.2
Requires (Dev)
- phpunit/phpunit: ^9.5
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 字段分开为 firstName 和 lastName
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
因为正常化器在反序列化时没有调用构造函数。