happyr / message-serializer
以正确方式序列化类。
Requires (Dev)
- symfony/messenger: ^4.4 || ^5.4 || ^6.0
- symfony/phpunit-bridge: ^4.4 || ^5.4 || ^6.0
Suggests
- symfony/messenger: if you want to use a Symfony messenger integration
README
此包包含一些接口和类,帮助您将PHP类序列化为数组。此包不会为您做任何魔法操作,而是帮助您自己定义序列化规则。
安装
composer require happyr/message-serializer
查看与Symfony Messenger的集成。
问题
当您将PHP类序列化以显示不同用户或应用的输出时,您应该特别注意的一点是。这个输出是公共契约的一部分,您不能更改它,否则可能会破坏其他应用。
考虑以下示例
class Foo { private $bar; public function getBar() { return $this->bar; } public function setBar($bar) { $this->bar = $bar; } } $x = new Foo(); $x->setBar('test string'); $output = serialize($x); echo $output;
这将输出
O:3:"Foo":1:{s:8:"Foobar";s:11:"test string";}
即使您使用json_encode
做了一些聪明的事情,您也会得到
{"bar":"test string"}
这乍一看可能没问题。但如果您稍微修改了Foo
类,比如重命名私有属性或添加另一个属性,那么您的输出将不同,您已经破坏了与用户的契约。
解决方案
为了避免这个问题,我们需要将类与纯表示形式分开。我们这样做的方式是使用一个Transformer
来从一个类生成一个数组。
use Happyr\MessageSerializer\Transformer\TransformerInterface; class FooTransformer implements TransformerInterface { public function getVersion(): int { return 1; } public function getIdentifier(): string { return 'foo'; } public function getPayload($message): array { return [ 'bar' => $message->getBar(), ]; } public function supportsTransform($message): bool { return $message instanceof Foo; } }
这个转换器只负责将Foo
类转换为数组。反向操作由Hydrator
处理。
use Happyr\MessageSerializer\Hydrator\HydratorInterface; class FooHydrator implements HydratorInterface { public function toMessage(array $payload, int $version) { $object = new Foo(); $object->setBar($payload['bar']); return $object; } public function supportsHydrate(string $identifier, int $version): bool { return $identifier === 'foo' && $version === 1; } }
有了转换器和注入器,您可以确保不会意外更改对用户的输出。
使用上面的Transformer
时,Foo
的文本表示将如下所示
{ "version": 1, "identifier": "foo", "timestamp": 1566491957, "payload": { "bar": "test string" }, "_meta": [] }
管理版本
如果您需要更改输出,可以使用版本属性来帮助。例如,假设您想将键bar
重命名为不同的名称。然后您可以创建一个新的Hydrator
,如下所示
use Happyr\MessageSerializer\Hydrator\HydratorInterface; class FooHydrator2 implements HydratorInterface { public function toMessage(array $payload, int $version) { $object = new Foo(); $object->setBar($payload['new_bar']); return $object; } public function supportsHydrate(string $identifier, int $version): bool { return $identifier === 'foo' && $version === 2; } }
现在,您只需更新转换器以符合您的新契约
use Happyr\MessageSerializer\Transformer\TransformerInterface; class FooTransformer implements TransformerInterface { public function getVersion(): int { return 2; } public function getIdentifier(): string { return 'foo'; } public function getPayload($message): array { return [ 'new_bar' => $message->getBar(), ]; } public function supportsTransform($message): bool { return $message instanceof Foo; } }
区分“无法注入消息”和“版本错误”
有时了解“我不想收到这条消息”和“我想收到这条消息,但不是这个版本”之间的区别很重要。一个例子场景是,当您有多个互相通信的应用程序时,您在使用消息失败传递/处理时的重试机制。您不希望在应用程序不感兴趣时重试消息,但如果消息版本有误(例如,当您更新了发送应用程序但没有更新接收应用程序时),您希望重试。
因此,让我们更新之前的示例中的FooHydrator2
use Happyr\MessageSerializer\Hydrator\Exception\VersionNotSupportedException; use Happyr\MessageSerializer\Hydrator\HydratorInterface; class FooHydrator2 implements HydratorInterface { // ... public function supportsHydrate(string $identifier, int $version): bool { if ('foo' !== $identifier) { return false; } if (2 === $version) { return true; } // We do support the message, but not the version throw new VersionNotSupportedException(); } }
SerializerRouter
如果您使用Happyr\MessageSerializer\Serializer
发送/消费消息,并且默认使用与Symfony messenger相同的传输,您可能想使用Happyr\MessageSerializer\SerializerRouter
。此序列化器将决定是否使用Happyr\MessageSerializer\Serializer
解码/编码您的消息或使用Symfony messenger的默认序列化器。
use Happyr\MessageSerializer\SerializerRouter; $serializerRouter = new SerializerRouter($happyrSerializer, $symfonySerializer);
与Symfony Messenger的集成
要使其与Symfony Messenger一起工作,请添加以下服务定义
# config/packages/happyr_message_serializer.yaml services: Happyr\MessageSerializer\Serializer: autowire: true Happyr\MessageSerializer\Transformer\MessageToArrayInterface: '@happyr.message_serializer.transformer' happyr.message_serializer.transformer: class: Happyr\MessageSerializer\Transformer\Transformer arguments: [!tagged happyr.message_serializer.transformer] Happyr\MessageSerializer\Hydrator\ArrayToMessageInterface: '@happyr.message_serializer.hydrator' happyr.message_serializer.hydrator: class: Happyr\MessageSerializer\Hydrator\Hydrator arguments: [!tagged happyr.message_serializer.hydrator] # If you want to use SerializerRouter Happyr\MessageSerializer\SerializerRouter: arguments: - '@Happyr\MessageSerializer\Serializer' - '@Symfony\Component\Messenger\Transport\Serialization\SerializerInterface'
如果您想自动标记所有转换器和注入器,请将以下内容添加到您的主服务文件中
# config/services.yaml services: # ... _instanceof: Happyr\MessageSerializer\Transformer\TransformerInterface: tags: - 'happyr.message_serializer.transformer' Happyr\MessageSerializer\Hydrator\HydratorInterface: tags: - 'happyr.message_serializer.hydrator'
然后最后,确保您配置了传输以使用此序列化器
# config/packages/messenger.yaml framework: messenger: transports: amqp: '%env(MESSENGER_TRANSPORT_DSN)%' to_foobar_application: dsn: '%env(MESSENGER_TRANSPORT_FOOBAR)%' serializer: 'Happyr\MessageSerializer\Serializer' # If you use SerializerRouter from_foobaz_application: dsn: '%env(MESSENGER_TRANSPORT_FOOBAZ)%' serializer: 'Happyr\MessageSerializer\SerializerRouter'
关于信封的说明
当使用Symfony Messenger时,您将获得一个Envelope
传递给TransformerInterface::getPayload()
。您需要这样处理它
use Happyr\MessageSerializer\Transformer\TransformerInterface; class FooTransformer implements TransformerInterface { // ... public function getPayload($message): array { if ($message instanceof Envelope) { $message = $message->getMessage(); } return [ 'bar' => $message->getBar(), ]; } public function supportsTransform($message): bool { if ($message instanceof Envelope) { $message = $message->getMessage(); } return $message instanceof Foo; } }
提示
您可以让您的消息实现HydratorInterface
和TransformerInterface
接口
use Happyr\MessageSerializer\Hydrator\HydratorInterface; use Happyr\MessageSerializer\Transformer\TransformerInterface; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use Symfony\Component\Messenger\Envelope; class CreateUser implements HydratorInterface, TransformerInterface { private $uuid; private $username; /** Constructor must be public and empty. */ public function __construct() {} public static function create(UuidInterface $uuid, string $username): self { $message = new self(); $message->uuid = $uuid; $message->username = $username; return $message; } public function getUuid(): UuidInterface { return $this->uuid; } public function getUsername(): string { return $this->username; } public function toMessage(array $payload, int $version): self { return self::create(Uuid::fromString($payload['id']), $payload['username']); } public function supportsHydrate(string $identifier, int $version): bool { return $identifier === 'create-user' && $version === 1; } public function getVersion(): int { return 1; } public function getIdentifier(): string { return 'create-user'; } public function getPayload($message): array { if ($message instanceof Envelope) { $message = $message->getMessage(); } return [ 'id' => $message->getUuid()->toString(), 'username' => $message->getUsername(), ]; } public function supportsTransform($message): bool { if ($message instanceof Envelope) { $message = $message->getMessage(); } return $message instanceof self; } }
请注意,我们不能使用构造函数来创建这个类的实例,因为它将同时作为值对象和服务使用。