happyr/message-serializer

以正确方式序列化类。

资助包维护!
Nyholm

0.5.2 2022-07-21 08:59 UTC

This package is auto-updated.

Last update: 2024-09-21 13:19:09 UTC


README

Latest Version Software License Code Coverage Quality Score Total Downloads

此包包含一些接口和类,帮助您将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;
    }
}

提示

您可以让您的消息实现HydratorInterfaceTransformerInterface接口

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;
    }
}

请注意,我们不能使用构造函数来创建这个类的实例,因为它将同时作为值对象和服务使用。