Skip to content

Instantly share code, notes, and snippets.

@simPod
Last active June 17, 2020 05:57
Show Gist options
  • Save simPod/b21cdd57c4873a2dadc4013c747a2fc9 to your computer and use it in GitHub Desktop.
Save simPod/b21cdd57c4873a2dadc4013c747a2fc9 to your computer and use it in GitHub Desktop.
WIP: JMS Serializer EnumHandler for myclabs/php-enum
<?php
declare(strict_types=1);
namespace NS;
use NS\Enum;
use NS\Exception\FileDoesntContainClassDefinition;
use NS\Exception\NotSupported;
use NS\PathnameToClass;
use NS\Kernel;
use Generator;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;
use SplFileInfo;
use Symfony\Component\Finder\Finder;
use Webmozart\Assert\Assert;
use function is_bool;
use function is_float;
use function is_int;
use function is_string;
use function is_subclass_of;
use function rtrim;
final class EnumHandler implements SubscribingHandlerInterface
{
private const PATH_PROPERTY_SEPARATOR = '::';
/** @return Generator<array<string, int|string>> */
public static function getSubscribingMethods() : Generator
{
$files = (new Finder())
->files()
->in(Kernel::getSrcDir())
->name('*.php');
$pathnameToClass = new PathnameToClass(Kernel::getSrcDir());
$formats = ['json', 'xml', 'yml'];
/** @var SplFileInfo $file */
foreach ($files as $file) {
try {
$class = $pathnameToClass->getClass($file);
} catch (FileDoesntContainClassDefinition $exception) {
continue;
}
if (! is_subclass_of($class, Enum::class)) {
continue;
}
foreach ($formats as $format) {
yield [
'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
'format' => $format,
'type' => $class,
'method' => 'serializeEnum',
];
yield [
'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
'format' => $format,
'type' => $class,
'method' => 'deserializeEnum',
];
}
}
}
/**
* @param mixed[] $typeMetadata
*
* @return mixed
*
* @psalm-template T
* @psalm-param Enum<T> $enum
*/
public function serializeEnum(SerializationVisitorInterface $visitor, Enum $enum, array $typeMetadata, Context $context)
{
$value = $enum->getValue();
if (is_int($value)) {
return $visitor->visitInteger($value, $typeMetadata);
}
if (is_string($value)) {
return $visitor->visitString($value, $typeMetadata);
}
if (is_bool($value)) {
return $visitor->visitBoolean($value, $typeMetadata);
}
if (is_float($value)) {
return $visitor->visitDouble($value, $typeMetadata);
}
throw NotSupported::new();
}
/**
* @param mixed $data
* @param mixed[] $type
*
* @psalm-template T
* @psalm-return Enum<T>
*/
public function deserializeEnum(
DeserializationVisitorInterface $visitor,
$data,
array $type,
Context $context
) : Enum {
$enumClass = $this->getEnumClass($type);
return $enumClass::get($data);
}
/**
* @param mixed[] $type
*
* @psalm-template T
* @psalm-return class-string<Enum<T>>
*/
private function getEnumClass(array $type) : string
{
/** @psalm-var class-string<Enum<T>> $enumClass */
$enumClass = $type['name'];
Assert::isAOf($enumClass, Enum::class);
return $enumClass;
}
private function getPropertyPath(Context $context) : string
{
$path = '';
$lastPropertyMetadata = null;
foreach ($context->getMetadataStack() as $element) {
if (! ($element instanceof PropertyMetadata)) {
continue;
}
$name = $element->name;
$path = '$' . $name . self::PATH_PROPERTY_SEPARATOR . $path;
$lastPropertyMetadata = $element;
}
if ($lastPropertyMetadata !== null) {
$path = $lastPropertyMetadata->class . self::PATH_PROPERTY_SEPARATOR . $path;
}
$path = rtrim($path, self::PATH_PROPERTY_SEPARATOR);
return $path;
}
}
@enumag
Copy link

enumag commented Jun 10, 2020

@simPod Well since both consistence/consistence-jms-serializer#14 and consistence/consistence-jms-serializer#15 were locked, we can continue here. Thank you for sharing! I'll try to adapt it into my project and see if I found anything worth changing.

@enumag
Copy link

enumag commented Jun 17, 2020

I didn't want the type searching logic so for my needs I simplified it to this:

use Exception;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;
use MyCLabs\Enum\Enum;

/**
 * Inspired by https://gist.github.com/simPod/b21cdd57c4873a2dadc4013c747a2fc9.
 */
final class EnumHandler implements SubscribingHandlerInterface
{
    public const TYPE_ENUM = 'enum';

    /**
     * @return mixed[][]
     */
    public static function getSubscribingMethods(): array
    {
        $formats = ['json', 'xml', 'yml'];
        $methods = [];
        foreach ($formats as $format) {
            $methods[] = [
                'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
                'type' => self::TYPE_ENUM,
                'format' => $format,
                'method' => 'serializeEnum',
            ];
            $methods[] = [
                'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
                'type' => self::TYPE_ENUM,
                'format' => $format,
                'method' => 'deserializeEnum',
            ];
        }

        return $methods;
    }

    /**
     * @param mixed[] $typeMetadata
     *
     * @return mixed
     *
     * @template T
     *
     * @param Enum<T> $enum
     */
    public function serializeEnum(SerializationVisitorInterface $visitor, Enum $enum, array $typeMetadata, Context $context)
    {
        $value = $enum->getValue();

        if (is_int($value)) {
            return $visitor->visitInteger($value, $typeMetadata);
        }

        if (is_string($value)) {
            return $visitor->visitString($value, $typeMetadata);
        }

        if (is_bool($value)) {
            return $visitor->visitBoolean($value, $typeMetadata);
        }

        if (is_float($value)) {
            return $visitor->visitDouble($value, $typeMetadata);
        }

        throw new Exception();
    }

    /**
     * @param mixed   $data
     * @param mixed[] $type
     *
     * @template T
     *
     * @return Enum<T>
     */
    public function deserializeEnum(
        DeserializationVisitorInterface $visitor,
        $data,
        array $type,
        Context $context
    ): Enum {
        $enumClass = $this->getEnumClass($type);

        return new $enumClass($data);
    }

    /**
     * @param mixed[] $type
     *
     * @return class-string
     */
    private function getEnumClass(array $type): string
    {
        /** @var class-string $enumClass */
        $enumClass = $type['params'][0]['name'];
        assert(is_subclass_of($enumClass, Enum::class));

        return $enumClass;
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment