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)