Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Allow deserializing null #1005

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/DeserializationContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ class DeserializationContext extends Context
*/
private $depth = 0;

/**
* @var bool
*/
private $deserializeNull = true;

public static function create(): self
{
return new self();
Expand All @@ -33,6 +38,25 @@ public function increaseDepth(): void
$this->depth += 1;
}

/**
* Set if NULLs should be deserialized (TRUE) ot not (FALSE)
*/
public function setDeserializeNull(bool $bool): self
{
$this->deserializeNull = $bool;

return $this;
}

/**
* Returns TRUE when NULLs should be deserialized
* Returns FALSE when NULLs should not be deserialized
*/
public function shouldDeserializeNull(): bool
{
return $this->deserializeNull;
}

public function decreaseDepth(): void
{
if ($this->depth <= 0) {
Expand Down
29 changes: 24 additions & 5 deletions src/GraphNavigator/DeserializationGraphNavigator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use JMS\Serializer\Accessor\AccessorStrategyInterface;
use JMS\Serializer\Construction\ObjectConstructorInterface;
use JMS\Serializer\Context;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\EventDispatcher\EventDispatcher;
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
Expand All @@ -23,6 +24,7 @@
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\NullAwareVisitorInterface;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\VisitorInterface;
use Metadata\MetadataFactoryInterface;

/**
Expand Down Expand Up @@ -74,6 +76,11 @@ final class DeserializationGraphNavigator extends GraphNavigator implements Grap
*/
private $accessor;

/**
* @var bool
*/
private $shouldDeserializeNull;
goetas marked this conversation as resolved.
Show resolved Hide resolved

public function __construct(
MetadataFactoryInterface $metadataFactory,
HandlerRegistryInterface $handlerRegistry,
Expand All @@ -92,6 +99,12 @@ public function __construct(
}
}

public function initialize(VisitorInterface $visitor, Context $context): void
{
parent::initialize($visitor, $context);
$this->shouldDeserializeNull = $context->shouldDeserializeNull();
}

/**
* Called for each node of the graph that is being traversed.
*
Expand All @@ -109,10 +122,13 @@ public function accept($data, ?array $type = null)
}
// Sometimes data can convey null but is not of a null type.
// Visitors can have the power to add this custom null evaluation
if ($this->visitor instanceof NullAwareVisitorInterface && true === $this->visitor->isNull($data)) {
// If null is explicitly allowed we should skip this
if ($this->visitor instanceof NullAwareVisitorInterface
&& true === $this->visitor->isNull($data)
&& true === $this->shouldDeserializeNull
) {
$type = ['name' => 'NULL', 'params' => []];
}

switch ($type['name']) {
case 'NULL':
return $this->visitor->visitNull($data, $type);
Expand Down Expand Up @@ -153,9 +169,11 @@ public function accept($data, ?array $type = null)
// before loading metadata because the type name might not be a class, but
// could also simply be an artifical type.
if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_DESERIALIZATION, $type['name'], $this->format)) {
$rs = \call_user_func($handler, $this->visitor, $data, $type, $this->context);
$this->context->decreaseDepth();

try {
$rs = \call_user_func($handler, $this->visitor, $data, $type, $this->context);
} finally {
$this->context->decreaseDepth();
}
return $rs;
}

Expand Down Expand Up @@ -201,6 +219,7 @@ public function accept($data, ?array $type = null)
$this->context->pushPropertyMetadata($propertyMetadata);
try {
$v = $this->visitor->visitProperty($propertyMetadata, $data);

$this->accessor->setValue($object, $v, $propertyMetadata, $this->context);
} catch (NotAcceptableException $e) {
}
Expand Down
12 changes: 10 additions & 2 deletions src/JsonDeserializationVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;

final class JsonDeserializationVisitor extends AbstractVisitor implements DeserializationVisitorInterface
final class JsonDeserializationVisitor extends AbstractVisitor implements NullAwareVisitorInterface, DeserializationVisitorInterface
{
/**
* @var int
Expand Down Expand Up @@ -169,7 +169,7 @@ public function visitProperty(PropertyMetadata $metadata, $data)
throw new NotAcceptableException();
}

return null !== $data[$name] ? $this->navigator->accept($data[$name], $metadata->type) : null;
return $this->navigator->accept($data[$name], $metadata->type);
}

/**
Expand Down Expand Up @@ -237,4 +237,12 @@ public function prepare($str)
throw new RuntimeException('Could not decode JSON.');
}
}

/**
* {@inheritdoc}
*/
public function isNull($value): bool
{
return null === $value;
}
}
10 changes: 8 additions & 2 deletions tests/Fixtures/InitializedBlogPostConstructor.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ class InitializedBlogPostConstructor implements ObjectConstructorInterface
{
private $fallback;

public function __construct()
/**
* @var BlogPost
*/
private $blogPost;

public function __construct(BlogPost $blogPost)
{
$this->fallback = new UnserializeObjectConstructor();
$this->blogPost = $blogPost;
}

public function construct(DeserializationVisitorInterface $visitor, ClassMetadata $metadata, $data, array $type, DeserializationContext $context): ?object
Expand All @@ -25,6 +31,6 @@ public function construct(DeserializationVisitorInterface $visitor, ClassMetadat
return $this->fallback->construct($visitor, $metadata, $data, $type, $context);
}

return new BlogPost('This is a nice title.', new Author('Foo Bar'), new \DateTime('2011-07-30 00:00', new \DateTimeZone('UTC')), new Publisher('Bar Foo'));
return $this->blogPost;
}
}
24 changes: 24 additions & 0 deletions tests/Fixtures/ObjectWithNullObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures;

use JMS\Serializer\Annotation\Type;

class ObjectWithNullObject
{
/**
* @var null
* @Type("NullObject")
*/
private $nullProperty;

/**
* @return null
*/
public function getNullProperty()
{
return $this->nullProperty;
}
}
71 changes: 66 additions & 5 deletions tests/Serializer/BaseSerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
use JMS\Serializer\Tests\Fixtures\ObjectWithIntListAndIntMap;
use JMS\Serializer\Tests\Fixtures\ObjectWithIterator;
use JMS\Serializer\Tests\Fixtures\ObjectWithLifecycleCallbacks;
use JMS\Serializer\Tests\Fixtures\ObjectWithNullObject;
use JMS\Serializer\Tests\Fixtures\ObjectWithNullProperty;
use JMS\Serializer\Tests\Fixtures\ObjectWithToString;
use JMS\Serializer\Tests\Fixtures\ObjectWithTypedArraySetter;
Expand Down Expand Up @@ -209,6 +210,25 @@ public function testDeserializeNullObject()
self::assertNull($dObj->getNullProperty());
}

public function testDeserializeNullObjectWithHandler()
{
if (!$this->hasDeserializer()) {
$this->markTestSkipped(sprintf('No deserializer available for format `%s`', $this->getFormat()));
}
$ctx = DeserializationContext::create()
->setDeserializeNull(false);

/** @var ObjectWithNullObject $dObj */
$dObj = $this->serializer->deserialize(
$this->getContent('simple_object_nullable'),
ObjectWithNullObject::class,
$this->getFormat(),
$ctx
);

self::assertSame('nullObject', $dObj->getNullProperty());
}

/**
* @expectedException \JMS\Serializer\Exception\NotAcceptableException
* @dataProvider getTypes
Expand Down Expand Up @@ -730,14 +750,13 @@ public function testBlogPost()

public function testDeserializingNull()
{
$objectConstructor = new InitializedBlogPostConstructor();
$post = new BlogPost('This is a nice title.', $author = new Author('Foo Bar'), new \DateTime('2011-07-30 00:00', new \DateTimeZone('UTC')), new Publisher('Bar Foo'));
$objectConstructor = new InitializedBlogPostConstructor($post);

$builder = SerializerBuilder::create();
$builder->setObjectConstructor($objectConstructor);
$this->serializer = $builder->build();

$post = new BlogPost('This is a nice title.', $author = new Author('Foo Bar'), new \DateTime('2011-07-30 00:00', new \DateTimeZone('UTC')), new Publisher('Bar Foo'));

$this->setField($post, 'author', null);
$this->setField($post, 'publisher', null);

Expand All @@ -751,7 +770,42 @@ public function testDeserializingNull()
self::assertAttributeSame(false, 'published', $deserialized);
self::assertAttributeSame(false, 'reviewed', $deserialized);
self::assertAttributeEquals(new ArrayCollection(), 'comments', $deserialized);
self::assertEquals(null, $this->getField($deserialized, 'author'));
self::assertAttributeSame(null, 'author', $deserialized);
self::assertAttributeSame(null, 'tag', $deserialized);
}
}

public function testDeserializingNullAllowed()
{
$savedPost = new BlogPost('This is a nice title.', $author = new Author('Foo Bar'), new \DateTime('2011-07-30 00:00', new \DateTimeZone('UTC')), $publisher = new Publisher('Bar Foo'));
$savedPost->addTag(new Tag('foo'));
$initialTag = $this->getField($savedPost, 'tag');

$objectConstructor = new InitializedBlogPostConstructor($savedPost);

$builder = SerializerBuilder::create();
$builder->setObjectConstructor($objectConstructor);
$this->serializer = $builder->build();

$post = clone $savedPost;
$this->setField($post, 'author', null);
$this->setField($post, 'publisher', null);
$this->setField($post, 'tag', null);

if ($this->hasDeserializer()) {
$ctx = DeserializationContext::create();
$ctx->setDeserializeNull(false);

$deserialized = $this->deserialize($this->serialize($post), get_class($post), $ctx);

self::assertEquals('2011-07-30T00:00:00+00:00', $this->getField($deserialized, 'createdAt')->format(\DateTime::ATOM));
self::assertAttributeEquals('This is a nice title.', 'title', $deserialized);
self::assertAttributeSame(false, 'published', $deserialized);
self::assertAttributeSame(false, 'reviewed', $deserialized);
self::assertAttributeEquals(new ArrayCollection(), 'comments', $deserialized);
self::assertAttributeEquals($author, 'author', $deserialized);
self::assertAttributeEquals($publisher, 'publisher', $deserialized);
self::assertAttributeEquals($initialTag, 'tag', $deserialized);
}
}

Expand Down Expand Up @@ -1668,7 +1722,14 @@ static function (DeserializationVisitorInterface $visitor, $data, $type, Context
return $list;
}
);

$this->handlerRegistry->registerHandler(
GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
'NullObject',
$this->getFormat(),
static function (DeserializationVisitorInterface $visitor, $data, $type, Context $context) {
return 'nullObject';
}
);
$this->dispatcher = new EventDispatcher();
$this->dispatcher->addSubscriber(new DoctrineProxySubscriber());

Expand Down
1 change: 0 additions & 1 deletion tests/Serializer/GraphNavigatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ protected function setUp()
->enableOriginalConstructor()
->setMethodsExcept(['getExclusionStrategy'])
->getMock();

$this->deserializationContext = $this->getMockBuilder(DeserializationContext::class)
->enableOriginalConstructor()
->setMethodsExcept(['getExclusionStrategy'])
Expand Down
5 changes: 5 additions & 0 deletions tests/Serializer/XmlSerializationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,11 @@ public function testDeserializingNull()
$this->markTestSkipped('Not supported in XML.');
}

public function testDeserializingNullAllowed()
{
$this->markTestSkipped('Not supported in XML.');
}

public function testObjectWithXmlNamespaces()
{
$object = new ObjectWithXmlNamespaces('This is a nice title.', 'Foo Bar', new \DateTime('2011-07-30 00:00', new \DateTimeZone('UTC')), 'en');
Expand Down