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
39 changes: 38 additions & 1 deletion doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,43 @@ The serializer would expect the metadata files to be named like the fully qualif
replaced with ``.``. So, if you class would be named ``Vendor\Package\Foo``, the metadata file would need to be located
at ``$someDir/Vendor.Package.Foo.(xml|yml)``. If not found, ``$someDir/Vendor.Package.(xml|yml)`` will be tried, then ``$someDir/Vendor.Package.(xml|yml)`` and so on. For more information, see the :doc:`reference <reference>`.

Context Strategy
----------------------------------------------

Context strategy allows you to change serialization configuration for specific serialization or deserialization.
This allow you to deviate from default configuration.

### Serializing null values::

use JMS\Serializer\SerializationContext;

$serializer->serialize(
$object,
'json',
SerializationContext::create()->setSerializeNull(true)
)

### Serializing with different property naming strategy::

use JMS\Serializer\SerializationContext;

$serializer->serialize(
$object,
'json',
SerializationContext::create()->setPropertyNamingStrategy(new SerializedNameAnnotationStrategy(new IdenticalPropertyNamingStrategy()))
)

### Deserializing with different property naming strategy::

use JMS\Serializer\SerializationContext;

$serializer->deserialize(
$jsonString,
Product:class,
'json',
DeserializationContext::create()->setPropertyNamingStrategy(new SerializedNameAnnotationStrategy(new IdenticalPropertyNamingStrategy()))
)

Setting a default SerializationContext factory
----------------------------------------------
To avoid to pass an instance of SerializationContext
Expand All @@ -103,4 +140,4 @@ a serialization context from your callable and use it.

You can also set a default DeserializationContextFactory with
``->setDeserializationContextFactory(function () { /* ... */ })``
to be used with methods ``deserialize()`` and ``fromArray()``.
to be used with methods ``deserialize()`` and ``fromArray()``.
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\MetadataFactoryInterface;

abstract class Context
Expand Down Expand Up @@ -53,6 +54,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 @@ -227,6 +233,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
20 changes: 13 additions & 7 deletions src/GraphNavigator/DeserializationGraphNavigator.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,15 +209,21 @@ 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) {
$cloned = clone $propertyMetadata;
$cloned->setter = null;
$this->accessor->setValue($object, $cloned->defaultValue, $cloned, $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 :)

$contextSpecificMetadata->setter = null;
$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 @@ -258,8 +258,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 @@ -268,8 +275,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: 0 additions & 5 deletions src/Metadata/Driver/AnnotationOrAttributeDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -290,11 +290,6 @@ public function loadMetadataForClass(\ReflectionClass $class): ?BaseClassMetadat
}
}

if (!$configured) {
// return null;
// uncomment the above line afetr a couple of months
}

return $classMetadata;
}

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
18 changes: 18 additions & 0 deletions tests/Benchmark/JsonContextNamingSerializationBench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace JMS\Serializer\Tests\Benchmark;

use JMS\Serializer\Naming\IdenticalPropertyNamingStrategy;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Tests\Benchmark\Performance\JsonSerializationBench;

class JsonContextNamingSerializationBench extends JsonSerializationBench
{
protected function createContext(): SerializationContext
{
/** @phpstan-ignore-next-line */
return parent::createContext()->setPropertyNamingStrategy(new IdenticalPropertyNamingStrategy());
}
}
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 static function expressionFunctionProvider()
{
return [
Expand Down
Loading