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

Serialization Context Naming Strategy #1394

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
20 changes: 20 additions & 0 deletions src/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use JMS\Serializer\Exclusion\VersionExclusionStrategy;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
use Metadata\MetadataFactory;
use Metadata\MetadataFactoryInterface;

Expand Down Expand Up @@ -54,6 +55,11 @@ abstract class Context
/** @var \SplStack */
private $metadataStack;

/**
* @var PropertyNamingStrategyInterface|null
*/
private $propertyNamingStrategy = null;

public function __construct()
{
$this->metadataStack = new \SplStack();
Expand Down Expand Up @@ -228,6 +234,20 @@ public function popPropertyMetadata(): void
}
}

public function setPropertyNamingStrategy(PropertyNamingStrategyInterface $propertyNamingStrategy): self
{
$this->assertMutable();

$this->propertyNamingStrategy = $propertyNamingStrategy;

return $this;
}

public function getPropertyNamingStrategy(): ?PropertyNamingStrategyInterface
{
return $this->propertyNamingStrategy;
}

public function popClassMetadata(): void
{
$metadata = $this->metadataStack->pop();
Expand Down
17 changes: 12 additions & 5 deletions src/GraphNavigator/DeserializationGraphNavigator.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,20 @@ public function accept($data, ?array $type = null)
continue;
}

$this->context->pushPropertyMetadata($propertyMetadata);
/** Metadata changes based on context, should not be cached */
$contextSpecificMetadata = $propertyMetadata;
if (null !== $this->context->getPropertyNamingStrategy()) {
dgafka marked this conversation as resolved.
Show resolved Hide resolved
$contextSpecificMetadata = clone $propertyMetadata;
$contextSpecificMetadata->serializedName = $this->context->getPropertyNamingStrategy()->translateName($propertyMetadata);
}

$this->context->pushPropertyMetadata($contextSpecificMetadata);
try {
$v = $this->visitor->visitProperty($propertyMetadata, $data);
$this->accessor->setValue($object, $v, $propertyMetadata, $this->context);
$v = $this->visitor->visitProperty($contextSpecificMetadata, $data);
$this->accessor->setValue($object, $v, $contextSpecificMetadata, $this->context);
} catch (NotAcceptableException $e) {
if (true === $propertyMetadata->hasDefault) {
$this->accessor->setValue($object, $propertyMetadata->defaultValue, $propertyMetadata, $this->context);
if (true === $contextSpecificMetadata->hasDefault) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid it can has some side effects if $this->context->getPropertyNamingStrategy() is null as we are not going to clone at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah true. Should we clone always or leave this part code as it was before?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would left it as it used to be :)

$this->accessor->setValue($object, $contextSpecificMetadata->defaultValue, $contextSpecificMetadata, $this->context);
}
}

Expand Down
13 changes: 10 additions & 3 deletions src/GraphNavigator/SerializationGraphNavigator.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,15 @@ public function accept($data, ?array $type = null)
continue;
}

/** Metadata changes based on context, should not be cached */
$contextSpecificMetadata = $propertyMetadata;
if (null !== $this->context->getPropertyNamingStrategy()) {
$contextSpecificMetadata = clone $propertyMetadata;
$contextSpecificMetadata->serializedName = $this->context->getPropertyNamingStrategy()->translateName($propertyMetadata);
dgafka marked this conversation as resolved.
Show resolved Hide resolved
}

try {
$v = $this->accessor->getValue($data, $propertyMetadata, $this->context);
$v = $this->accessor->getValue($data, $contextSpecificMetadata, $this->context);
} catch (UninitializedPropertyException $e) {
continue;
}
Expand All @@ -267,8 +274,8 @@ public function accept($data, ?array $type = null)
continue;
}

$this->context->pushPropertyMetadata($propertyMetadata);
$this->visitor->visitProperty($propertyMetadata, $v);
$this->context->pushPropertyMetadata($contextSpecificMetadata);
$this->visitor->visitProperty($contextSpecificMetadata, $v);
$this->context->popPropertyMetadata();
}

Expand Down
5 changes: 5 additions & 0 deletions src/Metadata/PropertyMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ class PropertyMetadata extends BasePropertyMetadata
*/
public $serializedName;

/**
* @var string|null
*/
public $preContextSerializedName;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to be not used anymore.


/**
* @var array|null
*/
Expand Down
6 changes: 3 additions & 3 deletions tests/Fixtures/CustomDeserializationObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

namespace JMS\Serializer\Tests\Fixtures;

use JMS\Serializer\Annotation\Type;
use JMS\Serializer\Annotation as Serializer;

class CustomDeserializationObject
{
/**
* @Type("string")
* @Serializer\Type("string")
*/
#[Type(name: 'string')]
#[Serializer\Type(name: 'string')]
public $someProperty;

public function __construct($value)
Expand Down
21 changes: 21 additions & 0 deletions tests/Fixtures/CustomDeserializationObjectWithInnerClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures;

use JMS\Serializer\Annotation as Serializer;

class CustomDeserializationObjectWithInnerClass
{
/**
* @Serializer\Type("JMS\Serializer\Tests\Fixtures\CustomDeserializationObject")
*/
#[Serializer\Type(name: CustomDeserializationObject::class)]
private $someProperty;

public function __construct(CustomDeserializationObject $value)
{
$this->someProperty = $value;
}
}
23 changes: 23 additions & 0 deletions tests/Fixtures/CustomDeserializationObjectWithSerializedName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Fixtures;

use JMS\Serializer\Annotation as Serializer;

class CustomDeserializationObjectWithSerializedName
{
/**
* @Serializer\Type("string")
* @Serializer\SerializedName("name")
*/
#[Serializer\Type(name: 'string')]
#[Serializer\SerializedName(name: 'name')]
public $someProperty;

public function __construct($value)
{
$this->someProperty = $value;
}
}
116 changes: 116 additions & 0 deletions tests/SerializerBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@
use JMS\Serializer\Exception\UnsupportedFormatException;
use JMS\Serializer\Expression\ExpressionEvaluator;
use JMS\Serializer\Handler\HandlerRegistry;
use JMS\Serializer\Naming\CamelCaseNamingStrategy;
use JMS\Serializer\Naming\IdenticalPropertyNamingStrategy;
use JMS\Serializer\Naming\SerializedNameAnnotationStrategy;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerBuilder;
use JMS\Serializer\Tests\Fixtures\CustomDeserializationObject;
use JMS\Serializer\Tests\Fixtures\CustomDeserializationObjectWithInnerClass;
use JMS\Serializer\Tests\Fixtures\CustomDeserializationObjectWithSerializedName;
use JMS\Serializer\Tests\Fixtures\DocBlockType\Collection\Details\ProductDescription;
use JMS\Serializer\Tests\Fixtures\DocBlockType\SingleClassFromDifferentNamespaceTypeHint;
use JMS\Serializer\Tests\Fixtures\PersonSecret;
Expand Down Expand Up @@ -201,6 +207,116 @@ public function testSetCallbackSerializationContextWithNotSerializeNull()
self::assertEquals('{"not_null":"ok"}', $result);
}

public function testSetCallbackSerializationContextWithIdenticalPropertyNamingStrategy()
{
$this->builder->setSerializationContextFactory(static function () {
return SerializationContext::create()
->setPropertyNamingStrategy(new IdenticalPropertyNamingStrategy());
});
$this->builder->setDeserializationContextFactory(static function () {
return DeserializationContext::create()
->setPropertyNamingStrategy(new IdenticalPropertyNamingStrategy());
});

$serializer = $this->builder
->build();

$object = new CustomDeserializationObject('johny');
$json = '{"someProperty":"johny"}';

self::assertEquals($json, $serializer->serialize($object, 'json'));
self::assertEquals($object, $serializer->deserialize($json, get_class($object), 'json'));
}

public function testUsingNoSerializationContextInSecondRun()
{
$serializer = $this->builder->build();
$object = new CustomDeserializationObjectWithSerializedName('johny');

$jsonWithCamelCase = '{"someProperty":"johny"}';
self::assertEquals($jsonWithCamelCase, $serializer->serialize($object, 'json', SerializationContext::create()->setPropertyNamingStrategy(new IdenticalPropertyNamingStrategy())));
self::assertEquals($object, $serializer->deserialize($jsonWithCamelCase, get_class($object), 'json', DeserializationContext::create()->setPropertyNamingStrategy(new IdenticalPropertyNamingStrategy())));

$jsonWithUnderscores = '{"name":"johny"}';
self::assertEquals($jsonWithUnderscores, $serializer->serialize($object, 'json'));
self::assertEquals($object, $serializer->deserialize($jsonWithUnderscores, get_class($object), 'json'));
}

public function testUsingSerializedNameStrategyInContext()
{
$serializer = $this->builder->build();
$object = new CustomDeserializationObjectWithSerializedName('johny');

$jsonWithUnderscores = '{"name":"johny"}';
self::assertEquals($jsonWithUnderscores, $serializer->serialize(
$object,
'json',
SerializationContext::create()->setPropertyNamingStrategy(new SerializedNameAnnotationStrategy(new IdenticalPropertyNamingStrategy()))
));
self::assertEquals($object, $serializer->deserialize(
$jsonWithUnderscores,
get_class($object),
'json',
DeserializationContext::create()->setPropertyNamingStrategy(new SerializedNameAnnotationStrategy(new IdenticalPropertyNamingStrategy()))
));
}

public function testSetCallbackSerializationContextWithCamelCaseStrategy()
{
$this->builder->setSerializationContextFactory(static function () {
return SerializationContext::create()
->setPropertyNamingStrategy(new CamelCaseNamingStrategy());
});

$serializer = $this->builder
->build();

$object = new CustomDeserializationObject('johny');
$json = '{"some_property":"johny"}';

self::assertEquals($json, $serializer->serialize($object, 'json'));
self::assertEquals($object, $serializer->deserialize($json, get_class($object), 'json'));
}

public function testSetCallbackSerializationContextOverridingDefaultStrategy()
{
$this->builder->setSerializationContextFactory(static function () {
return SerializationContext::create()
->setPropertyNamingStrategy(new IdenticalPropertyNamingStrategy());
});
$this->builder->setDeserializationContextFactory(static function () {
return DeserializationContext::create()
->setPropertyNamingStrategy(new IdenticalPropertyNamingStrategy());
});

$serializer = $this->builder
->setPropertyNamingStrategy(new CamelCaseNamingStrategy())
->build();

$object = new CustomDeserializationObject('johny');
$json = '{"someProperty":"johny"}';

self::assertEquals($json, $serializer->serialize($object, 'json'));
self::assertEquals($object, $serializer->deserialize($json, get_class($object), 'json'));
}

public function testSetCallbackSerializationContextWithIdenticalPropertyNamingForInnerClass()
{
$this->builder->setSerializationContextFactory(static function () {
return SerializationContext::create()
->setPropertyNamingStrategy(new CamelCaseNamingStrategy());
});

$serializer = $this->builder
->build();

$object = new CustomDeserializationObjectWithInnerClass(new CustomDeserializationObject('johny'));
$json = '{"some_property":{"some_property":"johny"}}';

self::assertEquals($json, $serializer->serialize($object, 'json'));
self::assertEquals($object, $serializer->deserialize($json, get_class($object), 'json'));
}

public function expressionFunctionProvider()
{
return [
Expand Down