2

Context

By using the JMS serializer library, I need to serialize/unserialize data which are internally represented by php backed enums.

What's the problem

I found a solution by using the SubscribingHandlerInterface interface, but I would like to simplify the process, by removing (if possible) a boilerplate class which has to be created for each new enum.

Actual working code, to be simplified

  • Example enum:
<?php

namespace App\Enum;

enum MyEnum: string
{
    case Hello = 'hello';
    case World = 'world';
}
  • This abstract class is here to minimize redundant code for the final classes (the ones which I would like to "remove"):
<?php

namespace App\Serializer;

use JMS\Serializer\GraphNavigator;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonDeserializationVisitor;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Visitor\SerializationVisitorInterface;
use LogicException;

abstract class AbstractEnumSerializer implements SubscribingHandlerInterface
{
    public static function getEnumClass(): string
    {
        throw new LogicException("Please implement this");
    }

    public static function getSubscribingMethods(): array
    {
        return [
            [
                'direction' => GraphNavigator::DIRECTION_DESERIALIZATION,
                'format' => 'json',
                'type' => static::getEnumClass(),
                'method' => 'deserializeFromJSON',
            ], [
                'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
                'format' => 'json',
                'type' => static::getEnumClass(),
                'method' => 'serializeToJSON',
            ],
        ];
    }

    public function deserializeFromJSON(JsonDeserializationVisitor $visitor, $data, array $type)
    {
        return static::getEnumClass()::tryFrom($data);
    }

    public function serializeToJSON(
        SerializationVisitorInterface $visitor,
        $enum,
        array $type,
        SerializationContext $context
    ): string
    {
        return $enum->value;
    }
}
  • Here is the class which I want to "remove", by preferring some kind of automatic generation/registration: it implements (de)serialization for the above example enum, but it's boilerplate code, needed for each new enum:
<?php

namespace App\Serializer;

use App\Enum\MyEnum;

class MyEnumSerializer extends AbstractEnumSerializer
{
    public static function getEnumClass(): string
    {
        return MyEnum::class;
    }
}

Question

Let's imagine that many php backed enums have to be (de)serialized; is it possible to avoid writing the MyEnumSerializer class for each enum, by preferring some kind of automatic generation/registration?

The main goal is to keep it simple to add new backed enums, while automatically implementing JMS serialization/deserialization for them.

yolenoyer
  • 8,797
  • 2
  • 27
  • 61

3 Answers3

0

It's hacky but this works at least for deserialization of backed enums as object properties.

public function deserializeEnum(
    JsonDeserializationVisitor $visitor,
    $value, 
    array $type,
    Context $context
) {
    $targetProperty = new ReflectionProperty(
        $visitor->getCurrentObject()::class,
        $context->getCurrentPath()[
            array_key_last($context->getCurrentPath())
        ]
    );
    $targetPropertyType = $targetProperty->getType()->getName();
    $targetPropertyNullable = $targetProperty->getType()->allowsNull();

    if ($value === null && $targetPropertyNullable) {
        return null;
    }

    return $targetPropertyType::from($value);
}

0

I'm not sure if the question is still actual, but you can do it in more simple way.

class EnumJsonSerializerHandler implements SubscribingHandlerInterface
{
    public static function getSubscribingMethods(): array
    {
        return [
            [
                'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                'format' => 'json',
                'type' => 'Enum',
                'method' => 'serialize',
            ],
            [
                'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
                'format' => 'json',
                'type' => 'Enum',
                'method' => 'deserialize',
            ],
        ];
    }

    public function serialize(JsonSerializationVisitor $visitor, Enum $value, array $type, Context $context): string
    {
        return $value->getValue();
    }

    public function deserialize(JsonDeserializationVisitor $visitor, ?string $valueAsString, array $type, Context $context): ?Enum
    {
        try {
            if (!$valueAsString) {
                return null;
            }

            if(! isset($type['params'][0]['name'])) {
                throw new SkipHandlerException('Provide Enum class in annotation Enum<Path\To\EnumClass>');
            }

            return new $type['params'][0]['name']($valueAsString);
        } catch (Throwable $ex) {
            throw new SkipHandlerException($ex->getMessage());
        }
    }
}

And after it, define the property with annotation

    /**
     * @JMS\Type("Enum<Full\Path\To\SpecificEnumClass>")
     */
    private ?SpecificEnumClass $status;

Hope this helps.

0

Using a custom handler you can do something like this:

/**
 * @Serializer\Type("Enum<'Namespace\SomeStatus'>")
 */
public ?SomeStatus $status = null;

Do note that the class name is between single quotes

The handlers:

$serializerBuilder->configureHandlers(function (HandlerRegistry $registry) {
    $registry->registerHandler(
        GraphNavigatorInterface::DIRECTION_SERIALIZATION,
        'Enum',
        'json',
        function (VisitorInterface $visitor, BackedEnum $object, array $type) {
            return $object->value;
        }
    );

    $registry->registerHandler(
        GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
        'Enum',
        'json',
        function (VisitorInterface $visitor, mixed $data, array $type) {
            $class = $type['params'][0];
            return $class::tryFrom($data);
        }
    );
});
A1rPun
  • 16,287
  • 7
  • 57
  • 90