Create a custom JMS Serializer handler for mapping values
Tags: php, jmsserializer, api, symfony
Published: 2023-04-24 18:27:31

---
title: Create a custom JMS Serializer handler for mapping values
published: true
description:
tags: php, #jmsserializer, api, symfony
series: Mapping FieldValueIDs for the payload of the Emarsys API
---

In the context of my article [Create a custom Symfony Normalizer for mapping values](https://dev.to/alaugks/create-a-custom-symfony-normalizer-for-mapping-values-4nc2), I also wanted to see if a custom normaliser could be implemented for the [JMS Serializer](https://jmsyst.com/libs/serializer), which I also use in many projects. For the JMS Serializer this is a handler.

## Create the handler

For the [custom handler](https://github.com/alaugks/article-serializer/blob/8f7be949c14a46e377d97ef35f1d990fce8324bb/app/src/Handler/MappingTableHandler.php), the interface [SubscribingHandlerInterface](https://github.com/schmittjoh/serializer/blob/4ff99e4cbba44cdbe57028ec2ccc874c8f61bbe9/src/Handler/SubscribingHandlerInterface.php) must be implemented. In the method `getSubscribingMethods()` the methods for the direction and formats are defined.

```php
<?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
<?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](https://github.com/alaugks/article-serializer/blob/8f7be949c14a46e377d97ef35f1d990fce8324bb/app/tests/Functional/ContactDtoTest.php) 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
<?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](https://github.com/schmittjoh/JMSSerializerBundle), the handler still needs to be registered if you are not using the [default services.yaml configuration](https://symfony.com/doc/current/service_container.html#service-container-services-load-example).


```yaml
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:


```php
$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
<?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](https://github.com/alaugks/article-serializer/tree/jms-serializer)

## Updates
* Series name defined (May 5th 2023)
* Update series name (May 8th 2023)
* Fix broken links (Dez 30th 2023)