good-php / serialization
可扩展的基于反射的序列化器,支持JSON和PHP原语格式
Requires
- php: >=8.2
- good-php/reflection: ^1.0
- php-ds/php-ds: ^1.3.0
- tenantcloud/php-standard: ^2.0
Requires (Dev)
- pestphp/pest: ^2.8
- phake/phake: ^4.2
- php-cs-fixer/shim: ~3.19.2
- phpstan/phpstan: ~1.10.21
- phpstan/phpstan-mockery: ^1.1
- phpstan/phpstan-phpunit: ^1.3
- phpstan/phpstan-webmozart-assert: ^1.2
- tenantcloud/php-cs-fixer-rule-sets: ~3.0.0
This package is auto-updated.
Last update: 2024-09-14 15:42:35 UTC
README
该概念类似于Moshi,一个Java/Kotlin序列化库 - 最少的努力,不牺牲可定制性,支持不同格式或易于使用。
以下是可以默认序列化和反序列化的内容
/** * @template T1 */ class Item { /** * @param BackedEnumStub[] $array * @param Collection<int, T1> * @param T1 $generic * @param NestedGeneric<int, T1> $nested */ public function __construct( // Scalars public readonly int $int, public readonly float $float, public readonly string $string, public readonly bool $bool, // Nullable and optional values public readonly ?string $nullableString, public readonly int|null|MissingValue $optional, // Custom property names #[SerializedName('two')] public readonly string $one, // Backed enums public readonly BackedEnumStub $backedEnum, // Generics and nested objects public readonly mixed $generic, public readonly NestedGenerics $nestedGeneric, // Arrays and Illuminate Collection of any type (with generics!) public readonly array $array, public readonly Collection $collection, // Dates public readonly DateTime $dateTime, public readonly Carbon $carbon, ) {} }
然后您可以将其转换为“原语”(标量和标量数组)或JSON
$primitiveAdapter = $serializer->adapter( PrimitiveTypeAdapter::class, NamedType::wrap(Item::class, [Carbon::class]) ); $primitiveAdapter->serialize(new Item(...)) // -> ['int' => 123, ...] $jsonAdapter = $serializer->adapter( JsonTypeAdapter::class, NamedType::wrap(Item::class, [PrimitiveType::int()]) ); $jsonAdapter->deserialize('{"int": 123, ...}') // -> new Item(123, ...)
自定义映射器
映射器是自定义类型序列化的最简单形式。您只需标记一个方法为 #[MapTo()]
或 #[MapFrom]
属性,指定问题中的类型作为第一个参数或返回类型,序列化器将自动处理剩余部分。单个映射器可以有任意多的映射方法。
final class DateTimeMapper { #[MapTo(PrimitiveTypeAdapter::class)] public function serialize(DateTime $value): string { return $value->format(DateTimeInterface::RFC3339_EXTENDED); } #[MapFrom(PrimitiveTypeAdapter::class)] public function deserialize(string $value): DateTime { return new DateTime($value); } } $serializer = (new SerializerBuilder()) ->addMapperLast(new DateTimeMapper()) ->build();
有了映射器,您甚至可以处理复杂类型 - 例如泛型或继承
final class ArrayMapper { #[MapTo(PrimitiveTypeAdapter::class)] public function to(array $value, Type $type, Serializer $serializer): array { $itemAdapter = $serializer->adapter(PrimitiveTypeAdapter::class, $type->arguments[1]); return array_map(fn ($item) => $itemAdapter->serialize($item), $value); } #[MapFrom(PrimitiveTypeAdapter::class)] public function from(array $value, Type $type, Serializer $serializer): array { $itemAdapter = $serializer->adapter(PrimitiveTypeAdapter::class, $type->arguments[1]); return array_map(fn ($item) => $itemAdapter->deserialize($item), $value); } } final class BackedEnumMapper { #[MapTo(PrimitiveTypeAdapter::class, new BaseTypeAcceptedByAcceptanceStrategy(BackedEnum::class))] public function to(BackedEnum $value): string|int { return $value->value; } #[MapFrom(PrimitiveTypeAdapter::class, new BaseTypeAcceptedByAcceptanceStrategy(BackedEnum::class))] public function from(string|int $value, Type $type): BackedEnum { $enumClass = $type->name; return $enumClass::tryFrom($value); } }
类型适配器工厂
除了满足大多数需求类型映射器之外,您还可以使用类型适配器工厂来精确控制每个类型的序列化方式。
想法如下:当构建序列化器时,您按照优先顺序添加您想要使用的所有工厂
(new SerializerBuilder()) ->addMapperLast(new TestMapper()) // then this one ->addFactoryLast(new TestFactory()) // and this one last ->addFactory(new TestFactory()) // attempted first
工厂有以下签名
public function create(string $typeAdapterType, Type $type, Attributes $attributes, Serializer $serializer): ?TypeAdapter
如果您返回 null
,则调用下一个工厂。否则,使用返回的类型适配器。
序列化完全是使用类型适配器工厂构建的。每个支持的原生类型也都有自己的工厂,可以通过 ->addFactoryLast()
来覆盖。类型映射器本质上也是高级适配器工厂。
这是如何使用它们的方法
class NullableTypeAdapterFactory implements TypeAdapterFactory { public function create(string $typeAdapterType, Type $type, Attributes $attributes, Serializer $serializer): ?TypeAdapter { if ($typeAdapterType !== PrimitiveTypeAdapter::class || !$type instanceof NullableType) { return null; } return new NullableTypeAdapter( $serializer->adapter($typeAdapterType, $type->innerType, $attributes), ); } } class NullableTypeAdapter implements PrimitiveTypeAdapter { public function __construct( private readonly PrimitiveTypeAdapter $delegate, ) { } public function serialize(mixed $value): mixed { if ($value === null) { return null; } return $this->delegate->serialize($value); } public function deserialize(mixed $value): mixed { if ($value === null) { return null; } return $this->delegate->deserialize($value); } }
在这个例子中,NullableTypeAdapterFactory
处理所有可空类型。当提供一个非可空类型时,它返回 null
。这意味着“队列”中的下一个类型适配器将被调用。当提供一个可空值时,它返回一个新类型适配器实例,该实例有两个方法: serialize
和 deserialize
。它们正是按其名称执行的。
键命名
默认情况下,序列化器保留键的命名,但这很容易自定义(按优先顺序)
- 使用
#[SerializedName]
属性指定自定义属性名称 - 使用
#[SerializedName]
属性按类指定自定义命名策略 - 指定自定义全局命名策略(使用内置之一或编写自己的)
以下是一个示例
(new SerializerBuilder())->namingStrategy(BuiltInNamingStrategy::SNAKE_CASE); // Uses snake_case by default class Item1 { public function __construct( public int $keyName, // appears as "key_name" in serialized data #[SerializedName('second_key')] public int $firstKey, // second_key #[SerializedName(BuiltInNamingStrategy::PASCAL_CASE)] public int $thirdKey, // THIRD_KEY ) {} } // Uses PASCAL_CASE by default #[SerializedName(BuiltInNamingStrategy::PASCAL_CASE)] class Item2 { public function __construct( public int $keyName, // KEY_NAME ) {} }
默认情况下,提供了 snake_case
、camelCase
和 PascalCase
的策略,但实现自己的策略也很简单
class PrefixedNaming implements NamingStrategy { public function __construct( private readonly string $prefix, ) {} public function translate(PropertyReflection $property): string { return $this->prefix . $property->name(); } } #[SerializedName(new PrefixedNaming('$'))] class SiftTrackData {}
必需、可为空、可选和默认值
默认情况下,如果序列化负载中缺少属性
- 可为空的属性设置为
null
- 具有默认值的属性使用默认值
- 可选属性设置为
MissingValue::INSTANCE
- 任何其他都抛出异常
以下是一个示例
class Item { public function __construct( public ?int $first, // set to null public bool $second = true, // set to true public Item $third = new Item(...), // set to Item instance public int|MissingValue $fourth, // set to MissingValue::INSTANCE public int $fifth, // required, throws if missing ) {} } // all keys missing -> throws for 'fifth' property $adapter->deserialize([]) // only required property -> uses null, default values and optional $adapter->deserialize(['fifth' => 123]); // all properties -> fills all values $adapter->deserialize(['first' => 123, 'second' => false, ...]);
展开
有时多个模型之间共享相同的键/类型集合。您可以使用继承来实现这一点,但我们相信组合优于继承,因此我们提供了一种简单的方法来实现相同的行为,而无需使用继承
以下是一个示例
class Pagination { public function __construct( public readonly int $perPage, public readonly int $total, ) {} } class UsersPaginatedList { public function __construct( #[Flatten] public readonly Pagination $pagination, /** @var User[] */ public readonly array $users, ) {} } // {"perPage": 25, "total": 100, "users": []} $adapter->serialize( new UsersPaginatedList( pagination: new Pagination(25, 100), users: [], ) );
错误处理
这预计将与客户端提供的数据一起使用,因此良好的错误描述是必须的。以下是您可能会遇到的一些错误
- 期望类型 'int' 的值,但得到 'string'
- 期望类型 'string' 的值,但得到 'NULL'
- 解析时间字符串(2020 dasd)失败,位置5(d):数据库中找不到时区
- 期望类型为 'string|int',但得到 'boolean'
- 期望的是 [one, two] 之一,但得到 'five'
- 无法映射键 '1' 处的项目:期望类型为 'string',但得到 'NULL'
- 无法映射键 '0' 处的项目:期望类型为 'string',但得到 'NULL'(以及更多错误)。
- 无法映射路径 'nested.field' 处的属性:期望类型为 'string',但得到 'integer'
所有这些都是一系列PHP异常,带有 previous
异常。除了这些消息外,还有所有抛出的异常及其必要信息。
更多格式
您可以使用自己的类型适配器添加对更多格式的支持。所有现有的适配器都可供您使用。
interface XmlTypeAdapter extends TypeAdapter {} final class FromPrimitiveXmlTypeAdapter implements XmlTypeAdapter { public function __construct( private readonly PrimitiveTypeAdapter $primitiveAdapter, ) { } public function serialize(mixed $value): mixed { return xml_encode($this->primitiveAdapter->serialize($value)); } public function deserialize(mixed $value): mixed { return $this->primitiveAdapter->deserialize(xml_decode($value)); } }
为什么选择这个而不是其他所有选项呢?
有其他一些选择,但它们都将缺少至少其中的一项
- 不依赖于继承,因此可以序列化第三方类
- 解析现有的PHPDoc信息,而不是通过属性重复它
- 支持泛型类型,这对于包装类型非常有用
- 通过映射器和类型适配器允许简单的扩展和复杂的扩展
- 为无效数据生成开发者友好的错误消息
- 正确处理可选(缺少键)和
null
值作为独立的概念 - 易于通过添加额外格式进行扩展
- 简单的内部结构:没有节点树,没有值包装器,没有PHP解析,没有固有限制