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

Add support for skippable (de)serialization handlers #1238

Merged
merged 3 commits into from
Jul 13, 2020
Merged
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
5 changes: 5 additions & 0 deletions doc/handlers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,8 @@ Also, this type of handler is registered via the builder object::
})
;

Skippable Subscribing Handlers
-------------------------------

In case you need to be able to fall back to the default deserialization behavior instead of using your custom
handler, you can simply throw a `SkipHandlerException` from you custom handler method to do so.
13 changes: 13 additions & 0 deletions src/Exception/SkipHandlerException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Exception;

/**
* Throw this exception from you custom (de)serialization handler
* in order to fallback to the default (de)serialization behavior.
*/
class SkipHandlerException extends RuntimeException
{
}
11 changes: 8 additions & 3 deletions src/GraphNavigator/DeserializationGraphNavigator.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use JMS\Serializer\Exception\LogicException;
use JMS\Serializer\Exception\NotAcceptableException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Exception\SkipHandlerException;
use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy;
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
use JMS\Serializer\GraphNavigator;
Expand Down Expand Up @@ -156,10 +157,14 @@ 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);
$this->context->decreaseDepth();

return $rs;
return $rs;
} catch (SkipHandlerException $e) {
// Skip handler, fallback to default behavior
}
}

/** @var ClassMetadata $metadata */
Expand Down
9 changes: 6 additions & 3 deletions src/GraphNavigator/SerializationGraphNavigator.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use JMS\Serializer\Exception\ExpressionLanguageRequiredException;
use JMS\Serializer\Exception\NotAcceptableException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Exception\SkipHandlerException;
use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy;
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
use JMS\Serializer\Functions;
Expand Down Expand Up @@ -195,13 +196,15 @@ public function accept($data, ?array $type = null)
if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format)) {
try {
$rs = \call_user_func($handler, $this->visitor, $data, $type, $this->context);
$this->context->stopVisiting($data);

return $rs;
} catch (SkipHandlerException $e) {
// Skip handler, fallback to default behavior
} catch (NotAcceptableException $e) {
$this->context->stopVisiting($data);
throw $e;
}
$this->context->stopVisiting($data);

return $rs;
}

/** @var ClassMetadata $metadata */
Expand Down
93 changes: 92 additions & 1 deletion tests/Serializer/GraphNavigatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
use JMS\Serializer\Accessor\DefaultAccessorStrategy;
use JMS\Serializer\Construction\ObjectConstructorInterface;
use JMS\Serializer\Construction\UnserializeObjectConstructor;
use JMS\Serializer\Context;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\EventDispatcher\EventDispatcher;
use JMS\Serializer\Exception\NotAcceptableException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Exception\SkipHandlerException;
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\GraphNavigator\DeserializationGraphNavigator;
use JMS\Serializer\GraphNavigator\SerializationGraphNavigator;
Expand All @@ -22,6 +25,7 @@
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
use JMS\Serializer\Visitor\SerializationVisitorInterface;
use JMS\Serializer\VisitorInterface;
use Metadata\MetadataFactory;
use PHPUnit\Framework\TestCase;

Expand Down Expand Up @@ -157,6 +161,54 @@ public function testExposeAcceptHandlerExceptionOnSerialization()
$navigator->accept($object, ['name' => $typeName, 'params' => []]);
}

public function testHandlerIsExecutedOnSerialization()
{
$object = new SerializableClass();
$this->handlerRegistry->registerSubscribingHandler(new TestSubscribingHandler());

$this->context->method('getFormat')->willReturn(TestSubscribingHandler::FORMAT);

$navigator = new SerializationGraphNavigator($this->metadataFactory, $this->handlerRegistry, $this->accessor, $this->dispatcher);
$navigator->initialize($this->serializationVisitor, $this->context);
$this->context->initialize(TestSubscribingHandler::FORMAT, $this->serializationVisitor, $navigator, $this->metadataFactory);

$rt = $navigator->accept($object, null);
$this->assertEquals('foobar', $rt);
}

/**
* @doesNotPerformAssertions
*/
public function testFilterableHandlerIsSkippedOnSerialization()
{
$object = new SerializableClass();
$this->handlerRegistry->registerSubscribingHandler(new TestSkippableSubscribingHandler());

$this->context->method('getFormat')->willReturn(TestSkippableSubscribingHandler::FORMAT);

$navigator = new SerializationGraphNavigator($this->metadataFactory, $this->handlerRegistry, $this->accessor, $this->dispatcher);
$navigator->initialize($this->serializationVisitor, $this->context);
$this->context->initialize(TestSkippableSubscribingHandler::FORMAT, $this->serializationVisitor, $navigator, $this->metadataFactory);

$navigator->accept($object, null);
}

public function testFilterableHandlerIsNotSkippedOnSerialization()
{
$object = new SerializableClass();
$this->handlerRegistry->registerSubscribingHandler(new TestSkippableSubscribingHandler(false));

$this->context->method('getFormat')->willReturn(TestSkippableSubscribingHandler::FORMAT);

$navigator = new SerializationGraphNavigator($this->metadataFactory, $this->handlerRegistry, $this->accessor, $this->dispatcher);
$navigator->initialize($this->serializationVisitor, $this->context);
$this->context->initialize(TestSkippableSubscribingHandler::FORMAT, $this->serializationVisitor, $navigator, $this->metadataFactory);

$this->expectException(NotAcceptableException::class);
$this->expectExceptionMessage(TestSkippableSubscribingHandler::EX_MSG);
$navigator->accept($object, null);
}

/**
* @doesNotPerformAssertions
*/
Expand Down Expand Up @@ -213,11 +265,50 @@ public static function getSubscribingMethods()
{
return [
[
'type' => 'JsonSerializable',
'type' => SerializableClass::class,
'format' => self::FORMAT,
'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
'method' => 'serialize',
],
];
}

public function serialize(VisitorInterface $visitor, $userData, array $type, Context $context)
{
return 'foobar';
}
}

class TestSkippableSubscribingHandler implements SubscribingHandlerInterface
{
public const FORMAT = 'foo';
public const EX_MSG = 'This method should be skipped!';

private $shouldSkip;

public function __construct(bool $shouldSkip = true)
{
$this->shouldSkip = $shouldSkip;
}

public static function getSubscribingMethods()
{
return [
[
'type' => SerializableClass::class,
'format' => self::FORMAT,
'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
'method' => 'serialize',
],
];
}

public function serialize(VisitorInterface $visitor, $userData, array $type, Context $context)
{
if ($this->shouldSkip) {
throw new SkipHandlerException();
}

throw new NotAcceptableException(self::EX_MSG);
}
}