Create a custom JMS Serializer handler for mapping values
AL
Posted on April 24, 2023
In the context of my article Create a custom Symfony Normalizer for mapping values, I also wanted to see if a custom normaliser could be implemented for the JMS Serializer, which I also use in many projects. For the JMS Serializer this is a handler.
Create the handler
For the custom handler, the interface SubscribingHandlerInterface must be implemented. In the method getSubscribingMethods()
the methods for the direction and formats are defined.
<?php declare(strict_types=1);
namespace App\Handler;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;
class MappingTableHandler implements SubscribingHandlerInterface
{
public const HANDLER_TYPE = 'MappingTable';
public static function getSubscribingMethods(): array
{
$methods = [];
foreach (['json', 'xml'] as $format) {
$methods[] = [
'type' => self::HANDLER_TYPE,
'format' => $format,
'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
'method' => 'serialize',
];
$methods[] = [
'type' => self::HANDLER_TYPE,
'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
'format' => $format,
'method' => 'deserialize',
];
}
return $methods;
}
public function normalize(
SerializationVisitorInterface $visitor,
string|bool|null $value,
array $type,
SerializationContext $context
): ?string
{
$mappingTable = $this->getMappingTable($type);
foreach ($mappingTable as $mKey => $mValue) {
if ($value === $mValue) {
return (string)$mKey; // Force string
}
}
return null;
}
public function denormalize(
DeserializationVisitorInterface $visitor,
$value,
array $type
): mixed
{
$mappingTable = $this->getMappingTable($type);
foreach ($mappingTable as $mKey => $mValue) {
if ((string)$value === (string)$mKey) {
return $mValue;
}
}
return null;
}
private function getMappingTable(array $type): array
{
$mappingTable = [];
if (!isset($type['params'][0])) {
throw new \InvalidArgumentException('mapping_table param not defined');
}
if ($array = json_decode($type['params'][0], true)) {
$mappingTable = $array;
}
return $mappingTable;
}
}
Define the FieldID and MappingTable
The FieldID is defined with the attribute #[SerializedName]
.
The #[Type]
attribute is used to define the MappingTable on the $salutation
and $marketingInformation
properties.
The MappingTables must be noted as a string.
Here is an example with JSON as MappingTable.
<?php declare(strict_types=1);
namespace App\Dto;
use JMS\Serializer\Annotation\SerializedName;
use JMS\Serializer\Annotation\Type;
class ContactDto
{
#[SerializedName('1')]
#[Type('string')]
private ?string $firstname = null;
#[SerializedName('2')]
#[Type('string')]
private ?string $lastname = null;
#[SerializedName('3')]
#[Type('string')]
private ?string $email = null;
#[SerializedName('4')]
#[Type("DateTime<'Y-m-d'>")]
private ?\DateTimeInterface $birthdate = null;
#[SerializedName('46')]
#[Type("MappingTable<'{\"1\": \"MALE\", \"2\": \"FEMALE\", \"6\": \"DIVERS\"}'>")]
private ?string $salutation = null;
#[SerializedName('100674')]
#[Type("MappingTable<'{\"1\": true, \"2\": false}'>")]
private ?bool $marketingInformation = null;
/* getter and setter */
}
I find the notation of masked JSON cumbersome and difficult to maintain.
Therefore, I have also implemented the possibility of notating a constant as a string in my implementation. It is still a string but easier to maintain.
In Symfony Forms, for example, we can use these constants.
I wrote a UnitTest that checks if the constants are available. Despite a very good IDE, it can happen that the string in Attribute #[Type]
is not renamed when the ContactDto or Constants are renamed.
<?php declare(strict_types=1);
namespace App\Dto;
use JMS\Serializer\Annotation\SerializedName;
use JMS\Serializer\Annotation\Type;
class ContactDto
{
/* other properties */
public const SALUTATION = ['1' => 'MALE', '2' => 'FEMALE', '3' => 'DIVERS'];
public const MARKETING_INFORMATION = ['1' => true, '2' => false];
#[SerializedName('46')]
#[Type("MappingTable<'App\Dto\ContactDto::SALUTATION'>")]
private ?string $salutation = null;
#[SerializedName('100674')]
#[Type("MappingTable<'App\Dto\ContactDto::MARKETING_INFORMATION'>")]
private ?bool $marketingInformation = null;
/* getter and setter */
}
Register the handler
Symfony JMSSerializerBundle
If you are using Symfony and the JMSSerializerBundle, the handler still needs to be registered if you are not using the default services.yaml configuration.
services:
app.handler.mapping_handler:
class: 'App\Handler\MappingTableHandler'
tags:
- { name: 'jms_serializer.handler', type: 'MappingTable', format: 'json' }
JMS Serializer standalone
If you use the JMS Serializer as a standalone library, you must register the handler as follows:
$serializer = SerializerBuilder::create()
->configureHandlers(function(HandlerRegistry $registry) {
$registry->registerSubscribingHandler(
new MappingTableHandler()
);
})
->build();
Normalize and denormalize
With the Serializer you can normalize (toArray()
), denormalize (fromArray()
), serialize (serialize()
) and deserialize (deserialize()
)
I use toArray()
and fromArray()
because I need an array for the API client:
<?php declare(strict_types = 1);
use JMS\Serializer\Serializer;
private $serializer Serializer
$contactDto = new ContactDto();
$contactDto->setSalutation('FEMALE');
$contactDto->setFirstname('Jane');
$contactDto->setLastname('Doe');
$contactDto->setEmail('jane.doe@example.com');
$contactDto->setBirthdate(new \DateTime('1989-11-09'));
$contactDto->setMarketingInformation(true);
// Normalize
$fields = $this->serializer->toArray($contactDto);
/*
Array
(
[1] => Jane
[2] => Doe
[3] => jane.doe@example.com
[4] => 1989-11-09
[46] => FEMALE
[100674] => true
)
*/
// Denormalize
$contactDto = $this->serializer->fromArray($fields, ContactDto::class);
/*
App\Dto\ContactDto Object
(
[firstname:App\Dto\ContactDto:private] => Jane
[lastname:App\Dto\ContactDto:private] => Doe
[email:App\Dto\ContactDto:private] => jane.doe@example.com
[birthdate:App\Dto\ContactDto:private] => DateTime Object
(
[date] => 1989-11-09 15:23:49.000000
[timezone_type] => 3
[timezone] => UTC
)
[salutation:App\Dto\ContactDto:private] => FEMALE
[marketingInformation:App\Dto\ContactDto:private] => 1
)
*/
Links
Full JMS Serializer handler on github
Updates
- Series name defined (May 5th 2023)
- Update series name (May 8th 2023)
- Fix broken links (Dez 30th 2023)
- Change GitHub Repository URL (Sep 4th 2024)
Posted on April 24, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.