diff --git a/doc/configuration.rst b/doc/configuration.rst index aabe95662..589c4741a 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -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 `. +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 @@ -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()``. \ No newline at end of file diff --git a/src/Context.php b/src/Context.php index 63fcfb6f4..4408ef4a1 100644 --- a/src/Context.php +++ b/src/Context.php @@ -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 @@ -53,6 +54,11 @@ abstract class Context /** @var \SplStack */ private $metadataStack; + /** + * @var PropertyNamingStrategyInterface|null + */ + private $propertyNamingStrategy = null; + public function __construct() { $this->metadataStack = new \SplStack(); @@ -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(); diff --git a/src/GraphNavigator/DeserializationGraphNavigator.php b/src/GraphNavigator/DeserializationGraphNavigator.php index a29e43c12..8f71dbd9c 100644 --- a/src/GraphNavigator/DeserializationGraphNavigator.php +++ b/src/GraphNavigator/DeserializationGraphNavigator.php @@ -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()) { + $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) { + $contextSpecificMetadata->setter = null; + $this->accessor->setValue($object, $contextSpecificMetadata->defaultValue, $contextSpecificMetadata, $this->context); } } diff --git a/src/GraphNavigator/SerializationGraphNavigator.php b/src/GraphNavigator/SerializationGraphNavigator.php index 1de6e3bb7..3250d92fc 100644 --- a/src/GraphNavigator/SerializationGraphNavigator.php +++ b/src/GraphNavigator/SerializationGraphNavigator.php @@ -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); + } + try { - $v = $this->accessor->getValue($data, $propertyMetadata, $this->context); + $v = $this->accessor->getValue($data, $contextSpecificMetadata, $this->context); } catch (UninitializedPropertyException $e) { continue; } @@ -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(); } diff --git a/src/Metadata/Driver/AnnotationOrAttributeDriver.php b/src/Metadata/Driver/AnnotationOrAttributeDriver.php index 8e1b52343..3642b973a 100644 --- a/src/Metadata/Driver/AnnotationOrAttributeDriver.php +++ b/src/Metadata/Driver/AnnotationOrAttributeDriver.php @@ -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; } diff --git a/src/Metadata/PropertyMetadata.php b/src/Metadata/PropertyMetadata.php index ff41c949f..377e41808 100644 --- a/src/Metadata/PropertyMetadata.php +++ b/src/Metadata/PropertyMetadata.php @@ -33,6 +33,11 @@ class PropertyMetadata extends BasePropertyMetadata */ public $serializedName; + /** + * @var string|null + */ + public $preContextSerializedName; + /** * @var array|null */ diff --git a/tests/Benchmark/JsonContextNamingSerializationBench.php b/tests/Benchmark/JsonContextNamingSerializationBench.php new file mode 100644 index 000000000..1229e8fe6 --- /dev/null +++ b/tests/Benchmark/JsonContextNamingSerializationBench.php @@ -0,0 +1,18 @@ +setPropertyNamingStrategy(new IdenticalPropertyNamingStrategy()); + } +} diff --git a/tests/Fixtures/CustomDeserializationObject.php b/tests/Fixtures/CustomDeserializationObject.php index 5d8dbdfab..96766df84 100644 --- a/tests/Fixtures/CustomDeserializationObject.php +++ b/tests/Fixtures/CustomDeserializationObject.php @@ -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) diff --git a/tests/Fixtures/CustomDeserializationObjectWithInnerClass.php b/tests/Fixtures/CustomDeserializationObjectWithInnerClass.php new file mode 100644 index 000000000..ec481fb52 --- /dev/null +++ b/tests/Fixtures/CustomDeserializationObjectWithInnerClass.php @@ -0,0 +1,21 @@ +someProperty = $value; + } +} diff --git a/tests/Fixtures/CustomDeserializationObjectWithSerializedName.php b/tests/Fixtures/CustomDeserializationObjectWithSerializedName.php new file mode 100644 index 000000000..5856c834d --- /dev/null +++ b/tests/Fixtures/CustomDeserializationObjectWithSerializedName.php @@ -0,0 +1,23 @@ +someProperty = $value; + } +} diff --git a/tests/SerializerBuilderTest.php b/tests/SerializerBuilderTest.php index 232bcfdd3..7b5a562bf 100644 --- a/tests/SerializerBuilderTest.php +++ b/tests/SerializerBuilderTest.php @@ -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; @@ -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 [