good-php/serialization

可扩展的基于反射的序列化器,支持JSON和PHP原语格式

v1.0.0 2024-05-14 14:48 UTC

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。这意味着“队列”中的下一个类型适配器将被调用。当提供一个可空值时,它返回一个新类型适配器实例,该实例有两个方法: serializedeserialize。它们正是按其名称执行的。

键命名

默认情况下,序列化器保留键的命名,但这很容易自定义(按优先顺序)

  • 使用 #[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_casecamelCasePascalCase 的策略,但实现自己的策略也很简单

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解析,没有固有限制