定制 happyr/message-serializer 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

happyr/message-serializer

Composer 安装命令:

composer require happyr/message-serializer

包简介

Serialize classes the good way.

README 文档

README

Latest Version Software License Code Coverage Quality Score Total Downloads

This package contains some interfaces and classes to help you serialize and deserialize a PHP class to an array. The package does not do any magic for you but rather help you to define your serialization rules yourself.

Install

composer require happyr/message-serializer

See integration with Symfony Messenger.

The Problem

When you serialize a PHP class to show the output for a different user or application there is one thing you should really keep in mind. That output is part of a public contract that you cannot change without possibly breaking other applications.

Consider this example:

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;

This will output:

O:3:"Foo":1:{s:8:"Foobar";s:11:"test string";}

Even if you doing something smart with json_encode you will get:

{"bar":"test string"}

This might seem fine at first. But if you change the Foo class slightly, say, rename the private property or add another property, then your output will differ and you have broken your contract with your users.

The solution

To avoid this problem we need to separate the class from the plain representation. The way we do that is to use a Transformer to take a class and produce an array.

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

This transformer is only responsible to convert a Foo class to an array. The reverse operation is handled by a 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;
    }
}

With transformers and hydrators you are sure to never accidentally change the output to the user.

The text representation of Foo when using the Transformer above will look like:

{
    "version": 1,
    "identifier": "foo",
    "timestamp": 1566491957,
    "payload": {
        "bar": "test string"
    },
    "_meta": []
}

Manage versions

If you need to change the output you may do so with help of the version property. As an example, say you want to rename the key bar to something differently. Then you create a new Hydrator like:

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

Now you simply update the transformer to your new contract:

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

Differentiate between "I cant hydrate message" and "Wrong version"

Sometimes it is important to know the difference between "I dont not want this message" and "I want this message, but not this version". An example scenario would be when you have multiple applications that communicate with each other and you are using a retry mechanism when a message failed to be delivered/handled. You do not want to retry a message if the application is not interested but you do want to retry if the message has wrong version (like it would be when you updated the sender app but not the receiver app).

So lets update FooHydrator2 from previous example:

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

If you dispatch/consume messages serialized with Happyr\MessageSerializer\Serializer and default Symfony messenger to same transport you might wanna use Happyr\MessageSerializer\SerializerRouter. This serializer will decide whether it will use Happyr\MessageSerializer\Serializer to decode/encode your message or the default one from Symfony messenger.

use Happyr\MessageSerializer\SerializerRouter;

$serializerRouter = new SerializerRouter($happyrSerializer, $symfonySerializer);

Integration with Symfony Messenger

To make it work with Symfony Messenger, add the following service definition:

# 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'

If you automatically want to tag all your Transformers and Hydrators, add this to your main service file:

# config/services.yaml
services:
    # ...

    _instanceof:
        Happyr\MessageSerializer\Transformer\TransformerInterface:
            tags:
                - 'happyr.message_serializer.transformer'

        Happyr\MessageSerializer\Hydrator\HydratorInterface:
            tags:
                - 'happyr.message_serializer.hydrator'

Then finally, make sure you configure your transport to use this serializer:

# 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'

Note about Envelopes

When using Symfony Messenger you will get an Envelope passed to TransformerInterface::getPayload(). You need to handle this like:

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

Pro tip

You can let your messages implement both HydratorInterface and 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;
    }
}

Just note that we cannot use a constructor to this class since it will work both as a value object and a service.

happyr/message-serializer 适用场景与选型建议

happyr/message-serializer 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 505.19k 次下载、GitHub Stars 达 81, 最近一次更新时间为 2019 年 06 月 20 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 happyr/message-serializer 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 happyr/message-serializer 我们能提供哪些服务?
定制开发 / 二次开发

基于 happyr/message-serializer 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 505.19k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 81
  • 点击次数: 25
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 81
  • Watchers: 3
  • Forks: 12
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2019-06-20