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.
composer require happyr/message-serializer
See integration with Symfony Messenger.
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;
publicfunctiongetBar()
{
return$this->bar;
}
publicfunctionsetBar($bar)
{
$this->bar = $bar;
}
}
$x = newFoo();
$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.
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.
useHappyr\MessageSerializer\Transformer\TransformerInterface;
class FooTransformer implements TransformerInterface
{
publicfunctiongetVersion(): int
{
return1;
}
publicfunctiongetIdentifier(): string
{
return'foo';
}
publicfunctiongetPayload($message): array
{
return [
'bar' => $message->getBar(),
];
}
publicfunctionsupportsTransform($message): bool
{
return$messageinstanceof Foo;
}
}This transformer is only responsible to convert a Foo class to an array. The
reverse operation is handled by a Hydrator:
useHappyr\MessageSerializer\Hydrator\HydratorInterface;
class FooHydrator implements HydratorInterface
{
publicfunctiontoMessage(array$payload, int$version)
{
$object = newFoo();
$object->setBar($payload['bar']);
return$object;
}
publicfunctionsupportsHydrate(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": []
}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:
useHappyr\MessageSerializer\Hydrator\HydratorInterface;
class FooHydrator2 implements HydratorInterface
{
publicfunctiontoMessage(array$payload, int$version)
{
$object = newFoo();
$object->setBar($payload['new_bar']);
return$object;
}
publicfunctionsupportsHydrate(string$identifier, int$version): bool
{
return$identifier === 'foo' && $version === 2;
}
}Now you simply update the transformer to your new contract:
useHappyr\MessageSerializer\Transformer\TransformerInterface;
class FooTransformer implements TransformerInterface
{
publicfunctiongetVersion(): int
{
return2;
}
publicfunctiongetIdentifier(): string
{
return'foo';
}
publicfunctiongetPayload($message): array
{
return [
'new_bar' => $message->getBar(),
];
}
publicfunctionsupportsTransform($message): bool
{
return$messageinstanceof Foo;
}
}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:
useHappyr\MessageSerializer\Hydrator\Exception\VersionNotSupportedException;
useHappyr\MessageSerializer\Hydrator\HydratorInterface;
class FooHydrator2 implements HydratorInterface
{
// ...publicfunctionsupportsHydrate(string$identifier, int$version): bool
{
if ('foo' !== $identifier) {
returnfalse;
}
if (2 === $version) {
returntrue;
}
// We do support the message, but not the versionthrownewVersionNotSupportedException();
}
}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.
useHappyr\MessageSerializer\SerializerRouter;
$serializerRouter = newSerializerRouter($happyrSerializer, $symfonySerializer);To make it work with Symfony Messenger, add the following service definition:
# config/packages/happyr_message_serializer.yamlservices:
Happyr\MessageSerializer\Serializer:
autowire: trueHappyr\MessageSerializer\Transformer\MessageToArrayInterface: '@happyr.message_serializer.transformer'happyr.message_serializer.transformer:
class: Happyr\MessageSerializer\Transformer\Transformerarguments: [!tagged happyr.message_serializer.transformer]Happyr\MessageSerializer\Hydrator\ArrayToMessageInterface: '@happyr.message_serializer.hydrator'happyr.message_serializer.hydrator:
class: Happyr\MessageSerializer\Hydrator\Hydratorarguments: [!tagged happyr.message_serializer.hydrator]# If you want to use SerializerRouterHappyr\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.yamlservices:
# ..._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.yamlframework:
messenger:
transports:
amqp: '%env(MESSENGER_TRANSPORT_DSN)%'to_foobar_application:
dsn: '%env(MESSENGER_TRANSPORT_FOOBAR)%'serializer: 'Happyr\MessageSerializer\Serializer'# If you use SerializerRouterfrom_foobaz_application:
dsn: '%env(MESSENGER_TRANSPORT_FOOBAZ)%'serializer: 'Happyr\MessageSerializer\SerializerRouter'When using Symfony Messenger you will get an Envelope passed to TransformerInterface::getPayload(). You need
to handle this like:
useHappyr\MessageSerializer\Transformer\TransformerInterface;
class FooTransformer implements TransformerInterface
{
// ...publicfunctiongetPayload($message): array
{
if ($messageinstanceof Envelope) {
$message = $message->getMessage();
}
return [
'bar' => $message->getBar(),
];
}
publicfunctionsupportsTransform($message): bool
{
if ($messageinstanceof Envelope) {
$message = $message->getMessage();
}
return$messageinstanceof Foo;
}
}You can let your messages implement both HydratorInterface and TransformerInterface:
useHappyr\MessageSerializer\Hydrator\HydratorInterface;
useHappyr\MessageSerializer\Transformer\TransformerInterface;
useRamsey\Uuid\Uuid;
useRamsey\Uuid\UuidInterface;
useSymfony\Component\Messenger\Envelope;
class CreateUser implements HydratorInterface, TransformerInterface
{
private$uuid;
private$username;
/** Constructor must be public and empty. */publicfunction__construct() {}
publicstaticfunctioncreate(UuidInterface$uuid, string$username): self
{
$message = newself();
$message->uuid = $uuid;
$message->username = $username;
return$message;
}
publicfunctiongetUuid(): UuidInterface
{
return$this->uuid;
}
publicfunctiongetUsername(): string
{
return$this->username;
}
publicfunctiontoMessage(array$payload, int$version): self
{
returnself::create(Uuid::fromString($payload['id']), $payload['username']);
}
publicfunctionsupportsHydrate(string$identifier, int$version): bool
{
return$identifier === 'create-user' && $version === 1;
}
publicfunctiongetVersion(): int
{
return1;
}
publicfunctiongetIdentifier(): string
{
return'create-user';
}
publicfunctiongetPayload($message): array
{
if ($messageinstanceof Envelope) {
$message = $message->getMessage();
}
return [
'id' => $message->getUuid()->toString(),
'username' => $message->getUsername(),
];
}
publicfunctionsupportsTransform($message): bool
{
if ($messageinstanceof Envelope) {
$message = $message->getMessage();
}
return$messageinstanceof self;
}
}Just note that we cannot use a constructor to this class since it will work both as a value object and a service.