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)
Top comments (0)