Skip to content

Commit

Permalink
feat(union): Add deserialisation of Union types from JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcin Czarnecki committed Jul 30, 2023
1 parent 363a140 commit 6532bda
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 16 deletions.
99 changes: 99 additions & 0 deletions src/Handler/UnionHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Handler;

use JMS\Serializer\Context;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;

final class UnionHandler implements SubscribingHandlerInterface
{
/**
* {@inheritdoc}
*/
public static function getSubscribingMethods()
{
return [
[
'type' => 'union',
'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
'format' => 'json',
'method' => 'deserializeUnion',
],
[
'type' => 'union',
'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
'format' => 'xml',
'method' => 'deserializeUnion',
],
[
'type' => 'union',
'format' => 'json',
'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
'method' => 'serializeUnion',
],
[
'type' => 'union',
'format' => 'xml',
'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
'method' => 'serializeUnion',
],
];
}

public function serializeUnion(
SerializationVisitorInterface $visitor,
$data,
array $type,
SerializationContext $context
) {
return $this->matchSimpleType($data, $type, $context);
}

/**
* @param int|string|\SimpleXMLElement $data
* @param array $type
*/
public function deserializeUnion(DeserializationVisitorInterface $visitor, $data, array $type, DeserializationContext $context)
{
if ($data instanceof \SimpleXMLElement) {
throw new RuntimeException('XML deserialisation into union types is not supported yet.');
}

return $this->matchSimpleType($data, $type, $context);
}

private function matchSimpleType($data, array $type, Context $context)
{
$dataType = gettype($data);
$alternativeName = null;
switch ($dataType) {
case 'boolean':
$alternativeName = 'bool';
break;
case 'integer':
$alternativeName = 'int';
break;
case 'double':
$alternativeName = 'float';
break;
case 'array':
case 'string':
break;
default:
throw new RuntimeException();
}

foreach ($type['params'] as $possibleType) {
if ($possibleType['name'] === $dataType || $possibleType['name'] === $alternativeName) {
return $context->getNavigator()->accept($data, $possibleType);
}
}
}
}
23 changes: 23 additions & 0 deletions src/Metadata/Driver/TypedPropertiesDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata
$type = $reflectionType->getName();

$propertyMetadata->setType($this->typeParser->parse($type));
} elseif ($this->shouldTypeHintUnion($reflectionType)) {
$propertyMetadata->setType([
'name' => 'union',
'params' => array_map(fn (string $type) => $this->typeParser->parse($type), $reflectionType->getTypes()),
]);
}
} catch (ReflectionException $e) {
continue;
Expand Down Expand Up @@ -139,4 +144,22 @@ private function shouldTypeHint(?ReflectionType $reflectionType): bool
return class_exists($reflectionType->getName())
|| interface_exists($reflectionType->getName());
}

/**
* @phpstan-assert-if-true \ReflectionUnionType $reflectionType
*/
private function shouldTypeHintUnion(?ReflectionType $reflectionType)
{
if (!$reflectionType instanceof \ReflectionUnionType) {
return false;
}

foreach ($reflectionType->getTypes() as $type) {
if ($this->shouldTypeHint($type)) {
return true;
}
}

return false;
}
}
5 changes: 5 additions & 0 deletions src/SerializerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
use JMS\Serializer\Handler\HandlerRegistryInterface;
use JMS\Serializer\Handler\IteratorHandler;
use JMS\Serializer\Handler\StdClassHandler;
use JMS\Serializer\Handler\UnionHandler;
use JMS\Serializer\Naming\CamelCaseNamingStrategy;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
use JMS\Serializer\Naming\SerializedNameAnnotationStrategy;
Expand Down Expand Up @@ -283,6 +284,10 @@ public function addDefaultHandlers(): self
$this->handlerRegistry->registerSubscribingHandler(new EnumHandler());
}

if (PHP_VERSION_ID >= 80000) {
$this->handlerRegistry->registerSubscribingHandler(new UnionHandler());
}

return $this;
}

Expand Down
14 changes: 13 additions & 1 deletion tests/Metadata/Driver/UnionTypedPropertiesDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,19 @@ public function testInferUnionTypesShouldResultInNoType()
$m = $this->resolve(UnionTypedProperties::class);

self::assertEquals(
null,
[
'name' => 'union',
'params' => [
[
'name' => 'string',
'params' => [],
],
[
'name' => 'int',
'params' => [],
],
],
],
$m->propertyMetadata['data']->type
);
}
Expand Down
18 changes: 3 additions & 15 deletions tests/Serializer/BaseSerializationTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use JMS\Serializer\Handler\IteratorHandler;
use JMS\Serializer\Handler\StdClassHandler;
use JMS\Serializer\Handler\SymfonyUidHandler;
use JMS\Serializer\Handler\UnionHandler;
use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Serializer;
Expand Down Expand Up @@ -1989,20 +1990,6 @@ public function testSerializingUnionTypedProperties()
self::assertEquals(static::getContent('data_integer'), $this->serialize($object));
}

public function testThrowingExceptionWhenDeserializingUnionProperties()
{
if (PHP_VERSION_ID < 80000) {
$this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class));

return;
}

$this->expectException(RuntimeException::class);

$object = new TypedProperties\UnionTypedProperties(10000);
self::assertEquals($object, $this->deserialize(static::getContent('data_integer'), TypedProperties\UnionTypedProperties::class));
}

public function testSerializingUnionDocBlockTypesProperties()
{
$object = new UnionTypedDocBLockProperty(10000);
Expand All @@ -2021,7 +2008,7 @@ public function testThrowingExceptionWhenDeserializingUnionDocBlockTypes()
$this->expectException(RuntimeException::class);

$object = new UnionTypedDocBLockProperty(10000);
self::assertEquals($object, $this->deserialize(static::getContent('data_integer'), TypedProperties\UnionTypedProperties::class));
self::assertEquals($object, $this->deserialize(static::getContent('data_integer'), UnionTypedDocBLockProperty::class));
}

public function testIterable(): void
Expand Down Expand Up @@ -2138,6 +2125,7 @@ protected function setUp(): void
$this->handlerRegistry->registerSubscribingHandler(new IteratorHandler());
$this->handlerRegistry->registerSubscribingHandler(new SymfonyUidHandler());
$this->handlerRegistry->registerSubscribingHandler(new EnumHandler());
$this->handlerRegistry->registerSubscribingHandler(new UnionHandler());
$this->handlerRegistry->registerHandler(
GraphNavigatorInterface::DIRECTION_SERIALIZATION,
'AuthorList',
Expand Down
14 changes: 14 additions & 0 deletions tests/Serializer/JsonSerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
use JMS\Serializer\EventDispatcher\ObjectEvent;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Tests\Fixtures\Author;
use JMS\Serializer\Tests\Fixtures\AuthorList;
use JMS\Serializer\Tests\Fixtures\FirstClassMapCollection;
use JMS\Serializer\Tests\Fixtures\ObjectWithEmptyArrayAndHash;
use JMS\Serializer\Tests\Fixtures\ObjectWithInlineArray;
use JMS\Serializer\Tests\Fixtures\Tag;
use JMS\Serializer\Tests\Fixtures\TypedProperties\UnionTypedProperties;
use JMS\Serializer\Visitor\Factory\JsonSerializationVisitorFactory;
use JMS\Serializer\Visitor\SerializationVisitorInterface;

Expand Down Expand Up @@ -434,6 +436,18 @@ public static function getTypeHintedArraysAndStdClass()
];
}

public function testDeserializingUnionProperties()
{
if (PHP_VERSION_ID < 80000) {
$this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class));

return;
}

$object = new UnionTypedProperties(10000);
self::assertEquals($object, $this->deserialize(static::getContent('data_integer'), UnionTypedProperties::class));
}

/**
* @param array $array
* @param string $expected
Expand Down
16 changes: 16 additions & 0 deletions tests/Serializer/XmlSerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use JMS\Serializer\Handler\DateHandler;
use JMS\Serializer\Handler\HandlerRegistryInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\Driver\TypedPropertiesDriver;
use JMS\Serializer\Metadata\StaticPropertyMetadata;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerBuilder;
Expand Down Expand Up @@ -43,6 +44,7 @@
use JMS\Serializer\Tests\Fixtures\PersonLocation;
use JMS\Serializer\Tests\Fixtures\SimpleClassObject;
use JMS\Serializer\Tests\Fixtures\SimpleSubClassObject;
use JMS\Serializer\Tests\Fixtures\TypedProperties\UnionTypedProperties;
use JMS\Serializer\Visitor\Factory\XmlDeserializationVisitorFactory;
use JMS\Serializer\Visitor\Factory\XmlSerializationVisitorFactory;
use JMS\Serializer\XmlSerializationVisitor;
Expand Down Expand Up @@ -597,6 +599,20 @@ public function testSerialisationWithPrecisionForFloat(): void
);
}

public function testThrowingExceptionWhenDeserializingUnionProperties()
{
if (PHP_VERSION_ID < 80000) {
$this->markTestSkipped(sprintf('%s requires PHP 8.0', TypedPropertiesDriver::class));

return;
}

$this->expectException(RuntimeException::class);

$object = new UnionTypedProperties(10000);
self::assertEquals($object, $this->deserialize(static::getContent('data_integer'), UnionTypedProperties::class));
}

private function xpathFirstToString(\SimpleXMLElement $xml, $xpath)
{
$nodes = $xml->xpath($xpath);
Expand Down

0 comments on commit 6532bda

Please sign in to comment.