Create a custom JMS Serializer handler for mapping values

alaugks

AL

Posted on April 24, 2023

Create a custom JMS Serializer handler for mapping values

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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 */
}
Enter fullscreen mode Exit fullscreen mode

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 */
}
Enter fullscreen mode Exit fullscreen mode

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' }
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
    )
*/
Enter fullscreen mode Exit fullscreen mode

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)
💖 💪 🙅 🚩
alaugks
AL

Posted on April 24, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related