[Serializer] Allow to provide (de)normalization context in mapping

This commit is contained in:
Maxime Steinhausser 2020-12-07 17:36:04 +01:00 committed by Fabien Potencier
parent d9f490a8b2
commit 7229fa1d8f
27 changed files with 1183 additions and 17 deletions

View File

@ -0,0 +1,93 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Annotation;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
* Annotation class for @Context().
*
* @Annotation
* @Target({"PROPERTY", "METHOD"})
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class Context
{
private $context;
private $normalizationContext;
private $denormalizationContext;
private $groups;
/**
* @throws InvalidArgumentException
*/
public function __construct(array $options = [], array $context = [], array $normalizationContext = [], array $denormalizationContext = [], array $groups = [])
{
if (!$context) {
if (!array_intersect((array_keys($options)), ['normalizationContext', 'groups', 'context', 'value', 'denormalizationContext'])) {
// gracefully supports context as first, unnamed attribute argument if it cannot be confused with Doctrine-style options
$context = $options;
} else {
// If at least one of the options match, it's likely to be Doctrine-style options. Search for the context inside:
$context = $options['value'] ?? $options['context'] ?? [];
}
}
$normalizationContext = $options['normalizationContext'] ?? $normalizationContext;
$denormalizationContext = $options['denormalizationContext'] ?? $denormalizationContext;
foreach (compact(['context', 'normalizationContext', 'denormalizationContext']) as $key => $value) {
if (!\is_array($value)) {
throw new InvalidArgumentException(sprintf('Option "%s" of annotation "%s" must be an array.', $key, static::class));
}
}
if (!$context && !$normalizationContext && !$denormalizationContext) {
throw new InvalidArgumentException(sprintf('At least one of the "context", "normalizationContext", or "denormalizationContext" options of annotation "%s" must be provided as a non-empty array.', static::class));
}
$groups = (array) ($options['groups'] ?? $groups);
foreach ($groups as $group) {
if (!\is_string($group)) {
throw new InvalidArgumentException(sprintf('Parameter "groups" of annotation "%s" must be a string or an array of strings. Got "%s".', static::class, get_debug_type($group)));
}
}
$this->context = $context;
$this->normalizationContext = $normalizationContext;
$this->denormalizationContext = $denormalizationContext;
$this->groups = $groups;
}
public function getContext(): array
{
return $this->context;
}
public function getNormalizationContext(): array
{
return $this->normalizationContext;
}
public function getDenormalizationContext(): array
{
return $this->denormalizationContext;
}
public function getGroups(): array
{
return $this->groups;
}
}

View File

@ -4,6 +4,7 @@ CHANGELOG
5.3
---
* Add the ability to provide (de)normalization context using metadata (e.g. `@Symfony\Component\Serializer\Annotation\Context`)
* deprecated `ArrayDenormalizer::setSerializer()`, call `setDenormalizer()` instead.
* added normalization formats to `UidNormalizer`

View File

@ -59,6 +59,24 @@ class AttributeMetadata implements AttributeMetadataInterface
*/
public $ignore = false;
/**
* @var array[] Normalization contexts per group name ("*" applies to all groups)
*
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getNormalizationContexts()} instead.
*/
public $normalizationContexts = [];
/**
* @var array[] Denormalization contexts per group name ("*" applies to all groups)
*
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getDenormalizationContexts()} instead.
*/
public $denormalizationContexts = [];
public function __construct(string $name)
{
$this->name = $name;
@ -138,6 +156,76 @@ class AttributeMetadata implements AttributeMetadataInterface
return $this->ignore;
}
/**
* {@inheritdoc}
*/
public function getNormalizationContexts(): array
{
return $this->normalizationContexts;
}
/**
* {@inheritdoc}
*/
public function getNormalizationContextForGroups(array $groups): array
{
$contexts = [];
foreach ($groups as $group) {
$contexts[] = $this->normalizationContexts[$group] ?? [];
}
return array_merge($this->normalizationContexts['*'] ?? [], ...$contexts);
}
/**
* {@inheritdoc}
*/
public function setNormalizationContextForGroups(array $context, array $groups = []): void
{
if (!$groups) {
$this->normalizationContexts['*'] = $context;
}
foreach ($groups as $group) {
$this->normalizationContexts[$group] = $context;
}
}
/**
* {@inheritdoc}
*/
public function getDenormalizationContexts(): array
{
return $this->denormalizationContexts;
}
/**
* {@inheritdoc}
*/
public function getDenormalizationContextForGroups(array $groups): array
{
$contexts = [];
foreach ($groups as $group) {
$contexts[] = $this->denormalizationContexts[$group] ?? [];
}
return array_merge($this->denormalizationContexts['*'] ?? [], ...$contexts);
}
/**
* {@inheritdoc}
*/
public function setDenormalizationContextForGroups(array $context, array $groups = []): void
{
if (!$groups) {
$this->denormalizationContexts['*'] = $context;
}
foreach ($groups as $group) {
$this->denormalizationContexts[$group] = $context;
}
}
/**
* {@inheritdoc}
*/
@ -157,6 +245,12 @@ class AttributeMetadata implements AttributeMetadataInterface
$this->serializedName = $attributeMetadata->getSerializedName();
}
// Overwrite only if both contexts are empty
if (!$this->normalizationContexts && !$this->denormalizationContexts) {
$this->normalizationContexts = $attributeMetadata->getNormalizationContexts();
$this->denormalizationContexts = $attributeMetadata->getDenormalizationContexts();
}
if ($ignore = $attributeMetadata->isIgnored()) {
$this->ignore = $ignore;
}
@ -169,6 +263,6 @@ class AttributeMetadata implements AttributeMetadataInterface
*/
public function __sleep()
{
return ['name', 'groups', 'maxDepth', 'serializedName', 'ignore'];
return ['name', 'groups', 'maxDepth', 'serializedName', 'ignore', 'normalizationContexts', 'denormalizationContexts'];
}
}

View File

@ -75,4 +75,34 @@ interface AttributeMetadataInterface
* Merges an {@see AttributeMetadataInterface} with in the current one.
*/
public function merge(self $attributeMetadata);
/**
* Gets all the normalization contexts per group ("*" being the base context applied to all groups).
*/
public function getNormalizationContexts(): array;
/**
* Gets the computed normalization contexts for given groups.
*/
public function getNormalizationContextForGroups(array $groups): array;
/**
* Sets the normalization context for given groups.
*/
public function setNormalizationContextForGroups(array $context, array $groups = []): void;
/**
* Gets all the denormalization contexts per group ("*" being the base context applied to all groups).
*/
public function getDenormalizationContexts(): array;
/**
* Gets the computed denormalization contexts for given groups.
*/
public function getDenormalizationContextForGroups(array $groups): array;
/**
* Sets the denormalization context for given groups.
*/
public function setDenormalizationContextForGroups(array $context, array $groups = []): void;
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Serializer\Mapping\Loader;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\Ignore;
@ -19,6 +20,7 @@ use Symfony\Component\Serializer\Annotation\MaxDepth;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Exception\MappingException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
@ -36,6 +38,7 @@ class AnnotationLoader implements LoaderInterface
Ignore::class => true,
MaxDepth::class => true,
SerializedName::class => true,
Context::class => true,
];
private $reader;
@ -83,6 +86,8 @@ class AnnotationLoader implements LoaderInterface
$attributesMetadata[$property->name]->setSerializedName($annotation->getSerializedName());
} elseif ($annotation instanceof Ignore) {
$attributesMetadata[$property->name]->setIgnore(true);
} elseif ($annotation instanceof Context) {
$this->setAttributeContextsForGroups($annotation, $attributesMetadata[$property->name]);
}
$loaded = true;
@ -130,6 +135,12 @@ class AnnotationLoader implements LoaderInterface
$attributeMetadata->setSerializedName($annotation->getSerializedName());
} elseif ($annotation instanceof Ignore) {
$attributeMetadata->setIgnore(true);
} elseif ($annotation instanceof Context) {
if (!$accessorOrMutator) {
throw new MappingException(sprintf('Context on "%s::%s()" cannot be added. Context can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name));
}
$this->setAttributeContextsForGroups($annotation, $attributeMetadata);
}
$loaded = true;
@ -166,4 +177,20 @@ class AnnotationLoader implements LoaderInterface
yield from $this->reader->getPropertyAnnotations($reflector);
}
}
private function setAttributeContextsForGroups(Context $annotation, AttributeMetadataInterface $attributeMetadata): void
{
if ($annotation->getContext()) {
$attributeMetadata->setNormalizationContextForGroups($annotation->getContext(), $annotation->getGroups());
$attributeMetadata->setDenormalizationContextForGroups($annotation->getContext(), $annotation->getGroups());
}
if ($annotation->getNormalizationContext()) {
$attributeMetadata->setNormalizationContextForGroups($annotation->getNormalizationContext(), $annotation->getGroups());
}
if ($annotation->getDenormalizationContext()) {
$attributeMetadata->setDenormalizationContextForGroups($annotation->getDenormalizationContext(), $annotation->getGroups());
}
}
}

View File

@ -74,6 +74,25 @@ class XmlFileLoader extends FileLoader
if (isset($attribute['ignore'])) {
$attributeMetadata->setIgnore((bool) $attribute['ignore']);
}
foreach ($attribute->context as $node) {
$groups = (array) $node->group;
$context = $this->parseContext($node->entry);
$attributeMetadata->setNormalizationContextForGroups($context, $groups);
$attributeMetadata->setDenormalizationContextForGroups($context, $groups);
}
foreach ($attribute->normalization_context as $node) {
$groups = (array) $node->group;
$context = $this->parseContext($node->entry);
$attributeMetadata->setNormalizationContextForGroups($context, $groups);
}
foreach ($attribute->denormalization_context as $node) {
$groups = (array) $node->group;
$context = $this->parseContext($node->entry);
$attributeMetadata->setDenormalizationContextForGroups($context, $groups);
}
}
if (isset($xml->{'discriminator-map'})) {
@ -136,4 +155,29 @@ class XmlFileLoader extends FileLoader
return $classes;
}
private function parseContext(\SimpleXMLElement $nodes): array
{
$context = [];
foreach ($nodes as $node) {
if (\count($node) > 0) {
if (\count($node->entry) > 0) {
$value = $this->parseContext($node->entry);
} else {
$value = [];
}
} else {
$value = XmlUtils::phpize($node);
}
if (isset($node['name'])) {
$context[(string) $node['name']] = $value;
} else {
$context[] = $value;
}
}
return $context;
}
}

View File

@ -101,6 +101,23 @@ class YamlFileLoader extends FileLoader
$attributeMetadata->setIgnore($data['ignore']);
}
foreach ($data['contexts'] ?? [] as $line) {
$groups = $line['groups'] ?? [];
if ($context = $line['context'] ?? false) {
$attributeMetadata->setNormalizationContextForGroups($context, $groups);
$attributeMetadata->setDenormalizationContextForGroups($context, $groups);
}
if ($context = $line['normalization_context'] ?? false) {
$attributeMetadata->setNormalizationContextForGroups($context, $groups);
}
if ($context = $line['denormalization_context'] ?? false) {
$attributeMetadata->setDenormalizationContextForGroups($context, $groups);
}
}
}
}

View File

@ -60,9 +60,12 @@
Contains serialization groups and max depth for attributes. The name of the attribute should be given in the "name" option.
]]></xsd:documentation>
</xsd:annotation>
<xsd:sequence minOccurs="0">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="group" type="xsd:string" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:element name="context" type="context" maxOccurs="unbounded" />
<xsd:element name="normalization_context" type="context" maxOccurs="unbounded" />
<xsd:element name="denormalization_context" type="context" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="max-depth">
<xsd:simpleType>
@ -81,4 +84,25 @@
<xsd:attribute name="ignore" type="xsd:boolean" />
</xsd:complexType>
<xsd:complexType name="context">
<xsd:choice maxOccurs="unbounded">
<xsd:element name="group" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="entry" type="context-root-entry" maxOccurs="unbounded" />
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="context-root-entry" mixed="true">
<xsd:sequence minOccurs="0">
<xsd:element name="entry" type="context-entry" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute type="xsd:string" name="name" use="required" />
</xsd:complexType>
<xsd:complexType name="context-entry" mixed="true">
<xsd:sequence minOccurs="0">
<xsd:element name="entry" type="context-entry" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute type="xsd:string" name="name" />
</xsd:complexType>
</xsd:schema>

View File

@ -237,8 +237,7 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
return false;
}
$tmpGroups = $context[self::GROUPS] ?? $this->defaultContext[self::GROUPS] ?? null;
$groups = (\is_array($tmpGroups) || is_scalar($tmpGroups)) ? (array) $tmpGroups : false;
$groups = $this->getGroups($context);
$allowedAttributes = [];
$ignoreUsed = false;
@ -250,14 +249,14 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
// If you update this check, update accordingly the one in Symfony\Component\PropertyInfo\Extractor\SerializerExtractor::getProperties()
if (
!$ignore &&
(false === $groups || array_intersect(array_merge($attributeMetadata->getGroups(), ['*']), $groups)) &&
([] === $groups || array_intersect(array_merge($attributeMetadata->getGroups(), ['*']), $groups)) &&
$this->isAllowedAttribute($classOrObject, $name = $attributeMetadata->getName(), null, $context)
) {
$allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata;
}
}
if (!$ignoreUsed && false === $groups && $allowExtraAttributes) {
if (!$ignoreUsed && [] === $groups && $allowExtraAttributes) {
// Backward Compatibility with the code using this method written before the introduction of @Ignore
return false;
}
@ -265,6 +264,13 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
return $allowedAttributes;
}
protected function getGroups(array $context): array
{
$groups = $context[self::GROUPS] ?? $this->defaultContext[self::GROUPS] ?? [];
return is_scalar($groups) ? (array) $groups : $groups;
}
/**
* Is this attribute allowed?
*

View File

@ -175,9 +175,10 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
continue;
}
$attributeValue = $this->getAttributeValue($object, $attribute, $format, $context);
$attributeContext = $this->getAttributeNormalizationContext($object, $attribute, $context);
$attributeValue = $this->getAttributeValue($object, $attribute, $format, $attributeContext);
if ($maxDepthReached) {
$attributeValue = $maxDepthHandler($attributeValue, $object, $attribute, $format, $context);
$attributeValue = $maxDepthHandler($attributeValue, $object, $attribute, $format, $attributeContext);
}
/**
@ -185,14 +186,14 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
*/
$callback = $context[self::CALLBACKS][$attribute] ?? $this->defaultContext[self::CALLBACKS][$attribute] ?? null;
if ($callback) {
$attributeValue = $callback($attributeValue, $object, $attribute, $format, $context);
$attributeValue = $callback($attributeValue, $object, $attribute, $format, $attributeContext);
}
if (null !== $attributeValue && !is_scalar($attributeValue)) {
$stack[$attribute] = $attributeValue;
}
$data = $this->updateData($data, $attribute, $attributeValue, $class, $format, $context);
$data = $this->updateData($data, $attribute, $attributeValue, $class, $format, $attributeContext);
}
foreach ($stack as $attribute => $attributeValue) {
@ -200,7 +201,10 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer.', $attribute));
}
$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute, $format)), $class, $format, $context);
$attributeContext = $this->getAttributeNormalizationContext($object, $attribute, $context);
$childContext = $this->createChildContext($attributeContext, $attribute, $format);
$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $childContext), $class, $format, $attributeContext);
}
if (isset($context[self::PRESERVE_EMPTY_OBJECTS]) && !\count($data)) {
@ -210,6 +214,39 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
return $data;
}
/**
* Computes the normalization context merged with current one. Metadata always wins over global context, as more specific.
*/
private function getAttributeNormalizationContext($object, string $attribute, array $context): array
{
if (null === $metadata = $this->getAttributeMetadata($object, $attribute)) {
return $context;
}
return array_merge($context, $metadata->getNormalizationContextForGroups($this->getGroups($context)));
}
/**
* Computes the denormalization context merged with current one. Metadata always wins over global context, as more specific.
*/
private function getAttributeDenormalizationContext(string $class, string $attribute, array $context): array
{
if (null === $metadata = $this->getAttributeMetadata($class, $attribute)) {
return $context;
}
return array_merge($context, $metadata->getDenormalizationContextForGroups($this->getGroups($context)));
}
private function getAttributeMetadata($objectOrClass, string $attribute): ?AttributeMetadataInterface
{
if (!$this->classMetadataFactory) {
return null;
}
return $this->classMetadataFactory->getMetadataFor($objectOrClass)->getAttributesMetadata()[$attribute] ?? null;
}
/**
* {@inheritdoc}
*/
@ -312,8 +349,10 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
$resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
foreach ($normalizedData as $attribute => $value) {
$attributeContext = $this->getAttributeDenormalizationContext($resolvedClass, $attribute, $context);
if ($this->nameConverter) {
$attribute = $this->nameConverter->denormalize($attribute, $resolvedClass, $format, $context);
$attribute = $this->nameConverter->denormalize($attribute, $resolvedClass, $format, $attributeContext);
}
if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($resolvedClass, $attribute, $format, $context)) {
@ -324,16 +363,16 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
continue;
}
if ($context[self::DEEP_OBJECT_TO_POPULATE] ?? $this->defaultContext[self::DEEP_OBJECT_TO_POPULATE] ?? false) {
if ($attributeContext[self::DEEP_OBJECT_TO_POPULATE] ?? $this->defaultContext[self::DEEP_OBJECT_TO_POPULATE] ?? false) {
try {
$context[self::OBJECT_TO_POPULATE] = $this->getAttributeValue($object, $attribute, $format, $context);
$attributeContext[self::OBJECT_TO_POPULATE] = $this->getAttributeValue($object, $attribute, $format, $attributeContext);
} catch (NoSuchPropertyException $e) {
}
}
$value = $this->validateAndDenormalize($resolvedClass, $attribute, $value, $format, $context);
$value = $this->validateAndDenormalize($resolvedClass, $attribute, $value, $format, $attributeContext);
try {
$this->setAttributeValue($object, $attribute, $value, $format, $context);
$this->setAttributeValue($object, $attribute, $value, $format, $attributeContext);
} catch (InvalidArgumentException $e) {
throw new NotNormalizableValueException(sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type), $e->getCode(), $e);
}

View File

@ -0,0 +1,217 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Annotation;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\VarDumper\Dumper\CliDumper;
use Symfony\Component\VarDumper\Test\VarDumperTestTrait;
/**
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class ContextTest extends TestCase
{
use VarDumperTestTrait;
protected function setUp(): void
{
$this->setUpVarDumper([], CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_TRAILING_COMMA);
}
/**
* @dataProvider provideTestThrowsOnEmptyContextData
*/
public function testThrowsOnEmptyContext(callable $factory)
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('At least one of the "context", "normalizationContext", or "denormalizationContext" options of annotation "Symfony\Component\Serializer\Annotation\Context" must be provided as a non-empty array.');
$factory();
}
public function provideTestThrowsOnEmptyContextData(): iterable
{
yield 'constructor: empty args' => [function () { new Context([]); }];
yield 'doctrine-style: value option as empty array' => [function () { new Context(['value' => []]); }];
yield 'doctrine-style: context option as empty array' => [function () { new Context(['context' => []]); }];
yield 'doctrine-style: context option not provided' => [function () { new Context(['groups' => ['group_1']]); }];
if (\PHP_VERSION_ID >= 80000) {
yield 'named args: empty context' => [function () {
eval('return new Symfony\Component\Serializer\Annotation\Context(context: []);');
}];
}
}
/**
* @dataProvider provideTestThrowsOnNonArrayContextData
*/
public function testThrowsOnNonArrayContext(array $options)
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('Option "%s" of annotation "%s" must be an array.', key($options), Context::class));
new Context($options);
}
public function provideTestThrowsOnNonArrayContextData(): iterable
{
yield 'non-array context' => [['context' => 'not_an_array']];
yield 'non-array normalization context' => [['normalizationContext' => 'not_an_array']];
yield 'non-array denormalization context' => [['normalizationContext' => 'not_an_array']];
}
public function testInvalidGroupOption()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('Parameter "groups" of annotation "%s" must be a string or an array of strings. Got "stdClass"', Context::class));
new Context(['context' => ['foo' => 'bar'], 'groups' => ['fine', new \stdClass()]]);
}
public function testInvalidGroupArgument()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('Parameter "groups" of annotation "%s" must be a string or an array of strings. Got "stdClass"', Context::class));
new Context([], ['foo' => 'bar'], [], [], ['fine', new \stdClass()]);
}
public function testAsFirstArg()
{
$context = new Context(['foo' => 'bar']);
self::assertSame(['foo' => 'bar'], $context->getContext());
self::assertEmpty($context->getNormalizationContext());
self::assertEmpty($context->getDenormalizationContext());
self::assertEmpty($context->getGroups());
}
public function testAsContextArg()
{
$context = new Context([], ['foo' => 'bar']);
self::assertSame(['foo' => 'bar'], $context->getContext());
self::assertEmpty($context->getNormalizationContext());
self::assertEmpty($context->getDenormalizationContext());
self::assertEmpty($context->getGroups());
}
/**
* @dataProvider provideValidInputs
*/
public function testValidInputs(callable $factory, string $expectedDump)
{
self::assertDumpEquals($expectedDump, $factory());
}
public function provideValidInputs(): iterable
{
yield 'doctrine-style: with context option' => [
function () { return new Context(['context' => ['foo' => 'bar']]); },
$expected = <<<DUMP
Symfony\Component\Serializer\Annotation\Context {
-context: [
"foo" => "bar",
]
-normalizationContext: []
-denormalizationContext: []
-groups: []
}
DUMP
];
yield 'constructor: with context arg' => [
function () { return new Context([], ['foo' => 'bar']); },
$expected,
];
yield 'doctrine-style: with normalization context option' => [
function () { return new Context(['normalizationContext' => ['foo' => 'bar']]); },
$expected = <<<DUMP
Symfony\Component\Serializer\Annotation\Context {
-context: []
-normalizationContext: [
"foo" => "bar",
]
-denormalizationContext: []
-groups: []
}
DUMP
];
yield 'constructor: with normalization context arg' => [
function () { return new Context([], [], ['foo' => 'bar']); },
$expected,
];
yield 'doctrine-style: with denormalization context option' => [
function () { return new Context(['denormalizationContext' => ['foo' => 'bar']]); },
$expected = <<<DUMP
Symfony\Component\Serializer\Annotation\Context {
-context: []
-normalizationContext: []
-denormalizationContext: [
"foo" => "bar",
]
-groups: []
}
DUMP
];
yield 'constructor: with denormalization context arg' => [
function () { return new Context([], [], [], ['foo' => 'bar']); },
$expected,
];
yield 'doctrine-style: with groups option as string' => [
function () { return new Context(['context' => ['foo' => 'bar'], 'groups' => 'a']); },
<<<DUMP
Symfony\Component\Serializer\Annotation\Context {
-context: [
"foo" => "bar",
]
-normalizationContext: []
-denormalizationContext: []
-groups: [
"a",
]
}
DUMP
];
yield 'doctrine-style: with groups option as array' => [
function () { return new Context(['context' => ['foo' => 'bar'], 'groups' => ['a', 'b']]); },
$expected = <<<DUMP
Symfony\Component\Serializer\Annotation\Context {
-context: [
"foo" => "bar",
]
-normalizationContext: []
-denormalizationContext: []
-groups: [
"a",
"b",
]
}
DUMP
];
yield 'constructor: with groups arg' => [
function () { return new Context([], ['foo' => 'bar'], [], [], ['a', 'b']); },
$expected,
];
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures\Annotations;
use Symfony\Component\Serializer\Annotation\Context;
/**
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class BadMethodContextDummy extends ContextDummyParent
{
/**
* @Context({ "foo" = "bar" })
*/
public function badMethod()
{
return 'bad_method';
}
}

View File

@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures\Annotations;
use Symfony\Component\Serializer\Annotation\Context;
/**
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class ContextDummy extends ContextDummyParent
{
/**
* @Context({ "foo" = "value", "bar" = "value", "nested" = {
* "nested_key" = "nested_value",
* }, "array": { "first", "second" } })
* @Context({ "bar" = "value_for_group_a" }, groups = "a")
*/
public $foo;
/**
* @Context(
* normalizationContext = { "format" = "d/m/Y" },
* denormalizationContext = { "format" = "m-d-Y H:i" },
* groups = {"a", "b"}
* )
*/
public $bar;
/**
* @Context(normalizationContext={ "prop" = "dummy_value" })
*/
public $overriddenParentProperty;
/**
* @Context({ "method" = "method_with_context" })
*/
public function getMethodWithContext()
{
return 'method_with_context';
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures\Annotations;
use Symfony\Component\Serializer\Annotation\Context;
/**
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class ContextDummyParent
{
/**
* @Context(normalizationContext={ "prop" = "dummy_parent_value" })
*/
public $parentProperty;
/**
* @Context(normalizationContext={ "prop" = "dummy_parent_value" })
*/
public $overriddenParentProperty;
}

View File

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures\Attributes;
use Symfony\Component\Serializer\Annotation\Context;
/**
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class BadMethodContextDummy extends ContextDummyParent
{
#[Context([ "foo" => "bar" ])]
public function badMethod()
{
return 'bad_method';
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures\Attributes;
use Symfony\Component\Serializer\Annotation\Context;
/**
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class ContextDummy extends ContextDummyParent
{
#[Context(['foo' => 'value', 'bar' => 'value', 'nested' => [
'nested_key' => 'nested_value'
], 'array' => ['first', 'second']])]
#[Context(context: ['bar' => 'value_for_group_a'], groups: ['a'])]
public $foo;
#[Context(
normalizationContext: ['format' => 'd/m/Y'],
denormalizationContext: ['format' => 'm-d-Y H:i'],
groups: ['a', 'b'],
)]
public $bar;
#[Context(normalizationContext: ['prop' => 'dummy_value'])]
public $overriddenParentProperty;
#[Context(['method' => 'method_with_context'])]
public function getMethodWithContext()
{
return 'method_with_context';
}
}

View File

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures\Attributes;
use Symfony\Component\Serializer\Annotation\Context;
/**
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class ContextDummyParent
{
#[Context(normalizationContext: ['prop' => 'dummy_parent_value'])]
public $parentProperty;
#[Context(normalizationContext: ['prop' => 'dummy_parent_value'])]
public $overriddenParentProperty;
}

View File

@ -39,4 +39,59 @@
<attribute name="ignored2" ignore="true" />
</class>
<class name="Symfony\Component\Serializer\Tests\Fixtures\Annotations\ContextDummyParent">
<attribute name="parentProperty">
<normalization_context>
<entry name="prop">dummy_parent_value</entry>
</normalization_context>
</attribute>
<attribute name="overriddenParentProperty">
<normalization_context>
<entry name="prop">dummy_parent_value</entry>
</normalization_context>
</attribute>
</class>
<class name="Symfony\Component\Serializer\Tests\Fixtures\Annotations\ContextDummy">
<attribute name="foo">
<context>
<entry name="foo">value</entry>
<entry name="bar">value</entry>
<entry name="nested">
<entry name="nested_key">nested_value</entry>
</entry>
<entry name="array">
<entry>first</entry>
<entry>second</entry>
</entry>
</context>
<context>
<group>a</group>
<entry name="bar">value_for_group_a</entry>
</context>
</attribute>
<attribute name="bar">
<normalization_context>
<group>a</group>
<group>b</group>
<entry name="format">d/m/Y</entry>
</normalization_context>
<denormalization_context>
<group>a</group>
<group>b</group>
<entry name="format">m-d-Y H:i</entry>
</denormalization_context>
</attribute>
<attribute name="overriddenParentProperty">
<normalization_context>
<entry name="prop">dummy_value</entry>
</normalization_context>
</attribute>
<attribute name="methodWithContext">
<context>
<entry name="method">method_with_context</entry>
</context>
</attribute>
</class>
</serializer>

View File

@ -30,3 +30,31 @@
ignore: true
ignored2:
ignore: true
Symfony\Component\Serializer\Tests\Fixtures\Annotations\ContextDummyParent:
attributes:
parentProperty:
contexts:
- { normalization_context: { prop: dummy_parent_value } }
overriddenParentProperty:
contexts:
- { normalization_context: { prop: dummy_parent_value } }
Symfony\Component\Serializer\Tests\Fixtures\Annotations\ContextDummy:
attributes:
foo:
contexts:
- context: { foo: value, bar: value, nested: { nested_key: nested_value }, array: [first, second] }
- context: { bar: value_for_group_a }
groups: [a]
bar:
contexts:
- normalization_context: { format: 'd/m/Y' }
denormalization_context: { format: 'm-d-Y H:i' }
groups: [a, b]
overriddenParentProperty:
contexts:
- normalization_context: { prop: dummy_value }
methodWithContext:
contexts:
- context: { method: method_with_context }

View File

@ -66,6 +66,57 @@ class AttributeMetadataTest extends TestCase
$this->assertTrue($attributeMetadata->isIgnored());
}
public function testSetContexts()
{
$metadata = new AttributeMetadata('a1');
$metadata->setNormalizationContextForGroups(['foo' => 'default', 'bar' => 'default'], []);
$metadata->setNormalizationContextForGroups(['foo' => 'overridden'], ['a', 'b']);
$metadata->setNormalizationContextForGroups(['bar' => 'overridden'], ['c']);
self::assertSame([
'*' => ['foo' => 'default', 'bar' => 'default'],
'a' => ['foo' => 'overridden'],
'b' => ['foo' => 'overridden'],
'c' => ['bar' => 'overridden'],
], $metadata->getNormalizationContexts());
$metadata->setDenormalizationContextForGroups(['foo' => 'default', 'bar' => 'default'], []);
$metadata->setDenormalizationContextForGroups(['foo' => 'overridden'], ['a', 'b']);
$metadata->setDenormalizationContextForGroups(['bar' => 'overridden'], ['c']);
self::assertSame([
'*' => ['foo' => 'default', 'bar' => 'default'],
'a' => ['foo' => 'overridden'],
'b' => ['foo' => 'overridden'],
'c' => ['bar' => 'overridden'],
], $metadata->getDenormalizationContexts());
}
public function testGetContextsForGroups()
{
$metadata = new AttributeMetadata('a1');
$metadata->setNormalizationContextForGroups(['foo' => 'default', 'bar' => 'default'], []);
$metadata->setNormalizationContextForGroups(['foo' => 'overridden'], ['a', 'b']);
$metadata->setNormalizationContextForGroups(['bar' => 'overridden'], ['c']);
self::assertSame(['foo' => 'default', 'bar' => 'default'], $metadata->getNormalizationContextForGroups([]));
self::assertSame(['foo' => 'overridden', 'bar' => 'default'], $metadata->getNormalizationContextForGroups(['a']));
self::assertSame(['foo' => 'overridden', 'bar' => 'default'], $metadata->getNormalizationContextForGroups(['b']));
self::assertSame(['foo' => 'default', 'bar' => 'overridden'], $metadata->getNormalizationContextForGroups(['c']));
self::assertSame(['foo' => 'overridden', 'bar' => 'overridden'], $metadata->getNormalizationContextForGroups(['b', 'c']));
$metadata->setDenormalizationContextForGroups(['foo' => 'default', 'bar' => 'default'], []);
$metadata->setDenormalizationContextForGroups(['foo' => 'overridden'], ['a', 'b']);
$metadata->setDenormalizationContextForGroups(['bar' => 'overridden'], ['c']);
self::assertSame(['foo' => 'default', 'bar' => 'default'], $metadata->getDenormalizationContextForGroups([]));
self::assertSame(['foo' => 'overridden', 'bar' => 'default'], $metadata->getDenormalizationContextForGroups(['a']));
self::assertSame(['foo' => 'overridden', 'bar' => 'default'], $metadata->getDenormalizationContextForGroups(['b']));
self::assertSame(['foo' => 'default', 'bar' => 'overridden'], $metadata->getDenormalizationContextForGroups(['c']));
self::assertSame(['foo' => 'overridden', 'bar' => 'overridden'], $metadata->getDenormalizationContextForGroups(['b', 'c']));
}
public function testMerge()
{
$attributeMetadata1 = new AttributeMetadata('a1');
@ -77,6 +128,8 @@ class AttributeMetadataTest extends TestCase
$attributeMetadata2->addGroup('c');
$attributeMetadata2->setMaxDepth(2);
$attributeMetadata2->setSerializedName('a3');
$attributeMetadata2->setNormalizationContextForGroups(['foo' => 'bar'], ['a']);
$attributeMetadata2->setDenormalizationContextForGroups(['baz' => 'qux'], ['c']);
$attributeMetadata2->setIgnore(true);
@ -85,9 +138,27 @@ class AttributeMetadataTest extends TestCase
$this->assertEquals(['a', 'b', 'c'], $attributeMetadata1->getGroups());
$this->assertEquals(2, $attributeMetadata1->getMaxDepth());
$this->assertEquals('a3', $attributeMetadata1->getSerializedName());
$this->assertSame(['a' => ['foo' => 'bar']], $attributeMetadata1->getNormalizationContexts());
$this->assertSame(['c' => ['baz' => 'qux']], $attributeMetadata1->getDenormalizationContexts());
$this->assertTrue($attributeMetadata1->isIgnored());
}
public function testContextsNotMergedIfAlreadyDefined()
{
$attributeMetadata1 = new AttributeMetadata('a1');
$attributeMetadata1->setNormalizationContextForGroups(['foo' => 'not overridden'], ['a']);
$attributeMetadata1->setDenormalizationContextForGroups(['baz' => 'not overridden'], ['b']);
$attributeMetadata2 = new AttributeMetadata('a2');
$attributeMetadata2->setNormalizationContextForGroups(['foo' => 'override'], ['a']);
$attributeMetadata2->setDenormalizationContextForGroups(['baz' => 'override'], ['b']);
$attributeMetadata1->merge($attributeMetadata2);
self::assertSame(['a' => ['foo' => 'not overridden']], $attributeMetadata1->getNormalizationContexts());
self::assertSame(['b' => ['baz' => 'not overridden']], $attributeMetadata1->getDenormalizationContexts());
}
public function testSerialize()
{
$attributeMetadata = new AttributeMetadata('attribute');

View File

@ -12,11 +12,13 @@
namespace Symfony\Component\Serializer\Tests\Mapping\Loader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Exception\MappingException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface;
use Symfony\Component\Serializer\Tests\Mapping\Loader\Features\ContextMappingTestTrait;
use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
/**
@ -24,6 +26,8 @@ use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
*/
abstract class AnnotationLoaderTest extends TestCase
{
use ContextMappingTestTrait;
/**
* @var AnnotationLoader
*/
@ -114,7 +118,31 @@ abstract class AnnotationLoaderTest extends TestCase
$this->assertTrue($attributesMetadata['ignored2']->isIgnored());
}
public function testLoadContexts()
{
$this->assertLoadedContexts($this->getNamespace().'\ContextDummy', $this->getNamespace().'\ContextDummyParent');
}
public function testThrowsOnContextOnInvalidMethod()
{
$class = $this->getNamespace().'\BadMethodContextDummy';
$this->expectException(MappingException::class);
$this->expectExceptionMessage(sprintf('Context on "%s::badMethod()" cannot be added', $class));
$loader = $this->getLoaderForContextMapping();
$classMetadata = new ClassMetadata($class);
$loader->loadClassMetadata($classMetadata);
}
abstract protected function createLoader(): AnnotationLoader;
abstract protected function getNamespace(): string;
protected function getLoaderForContextMapping(): LoaderInterface
{
return $this->loader;
}
}

View File

@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Mapping\Loader\Features;
use PHPUnit\Framework\Assert;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface;
use Symfony\Component\Serializer\Tests\Fixtures\Annotations\ContextDummy;
use Symfony\Component\Serializer\Tests\Fixtures\Annotations\ContextDummyParent;
/**
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
trait ContextMappingTestTrait
{
abstract protected function getLoaderForContextMapping(): LoaderInterface;
public function testLoadContexts()
{
$this->assertLoadedContexts();
}
public function assertLoadedContexts(string $dummyClass = ContextDummy::class, string $parentClass = ContextDummyParent::class)
{
$loader = $this->getLoaderForContextMapping();
$classMetadata = new ClassMetadata($dummyClass);
$parentClassMetadata = new ClassMetadata($parentClass);
$loader->loadClassMetadata($parentClassMetadata);
$classMetadata->merge($parentClassMetadata);
$loader->loadClassMetadata($classMetadata);
$attributes = $classMetadata->getAttributesMetadata();
Assert::assertEquals(['*' => ['prop' => 'dummy_parent_value']], $attributes['parentProperty']->getNormalizationContexts());
Assert::assertEquals(['*' => ['prop' => 'dummy_value']], $attributes['overriddenParentProperty']->getNormalizationContexts());
Assert::assertEquals([
'*' => [
'foo' => 'value',
'bar' => 'value',
'nested' => ['nested_key' => 'nested_value'],
'array' => ['first', 'second'],
],
'a' => ['bar' => 'value_for_group_a'],
], $attributes['foo']->getNormalizationContexts());
Assert::assertSame(
$attributes['foo']->getNormalizationContexts(),
$attributes['foo']->getDenormalizationContexts()
);
Assert::assertEquals([
'a' => $c = ['format' => 'd/m/Y'],
'b' => $c,
], $attributes['bar']->getNormalizationContexts());
Assert::assertEquals([
'a' => $c = ['format' => 'm-d-Y H:i'],
'b' => $c,
], $attributes['bar']->getDenormalizationContexts());
Assert::assertEquals(['*' => ['method' => 'method_with_context']], $attributes['methodWithContext']->getNormalizationContexts());
Assert::assertEquals(
$attributes['methodWithContext']->getNormalizationContexts(),
$attributes['methodWithContext']->getDenormalizationContexts()
);
}
}

View File

@ -21,6 +21,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummy;
use Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummyFirstChild;
use Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummySecondChild;
use Symfony\Component\Serializer\Tests\Fixtures\Annotations\IgnoreDummy;
use Symfony\Component\Serializer\Tests\Mapping\Loader\Features\ContextMappingTestTrait;
use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
/**
@ -28,10 +29,13 @@ use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
*/
class XmlFileLoaderTest extends TestCase
{
use ContextMappingTestTrait;
/**
* @var XmlFileLoader
*/
private $loader;
/**
* @var ClassMetadata
*/
@ -104,4 +108,9 @@ class XmlFileLoaderTest extends TestCase
$this->assertTrue($attributesMetadata['ignored1']->isIgnored());
$this->assertTrue($attributesMetadata['ignored2']->isIgnored());
}
protected function getLoaderForContextMapping(): LoaderInterface
{
return $this->loader;
}
}

View File

@ -22,6 +22,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummy;
use Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummyFirstChild;
use Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummySecondChild;
use Symfony\Component\Serializer\Tests\Fixtures\Annotations\IgnoreDummy;
use Symfony\Component\Serializer\Tests\Mapping\Loader\Features\ContextMappingTestTrait;
use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
/**
@ -29,6 +30,8 @@ use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
*/
class YamlFileLoaderTest extends TestCase
{
use ContextMappingTestTrait;
/**
* @var YamlFileLoader
*/
@ -126,4 +129,9 @@ class YamlFileLoaderTest extends TestCase
(new YamlFileLoader(__DIR__.'/../../Fixtures/invalid-ignore.yml'))->loadClassMetadata(new ClassMetadata(IgnoreDummy::class));
}
protected function getLoaderForContextMapping(): LoaderInterface
{
return $this->loader;
}
}

View File

@ -0,0 +1,92 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Normalizer\Features;
use Doctrine\Common\Annotations\AnnotationReader;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
/**
* Test context handling from Serializer metadata.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
trait ContextMetadataTestTrait
{
public function testContextMetadataNormalize()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new PhpDocExtractor());
new Serializer([new DateTimeNormalizer(), $normalizer]);
$dummy = new ContextMetadataDummy();
$dummy->date = new \DateTime('2011-07-28T08:44:00.123+00:00');
self::assertEquals(['date' => '2011-07-28T08:44:00+00:00'], $normalizer->normalize($dummy));
self::assertEquals(['date' => '2011-07-28T08:44:00.123+00:00'], $normalizer->normalize($dummy, null, [
ObjectNormalizer::GROUPS => 'extended',
]), 'a specific normalization context is used for this group');
self::assertEquals(['date' => '2011-07-28T08:44:00+00:00'], $normalizer->normalize($dummy, null, [
ObjectNormalizer::GROUPS => 'simple',
]), 'base denormalization context is unchanged for this group');
}
public function testContextMetadataContextDenormalize()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new PhpDocExtractor());
new Serializer([new DateTimeNormalizer(), $normalizer]);
/** @var ContextMetadataDummy $dummy */
$dummy = $normalizer->denormalize(['date' => '2011-07-28T08:44:00+00:00'], ContextMetadataDummy::class);
self::assertEquals(new \DateTime('2011-07-28T08:44:00+00:00'), $dummy->date);
/** @var ContextMetadataDummy $dummy */
$dummy = $normalizer->denormalize(['date' => '2011-07-28T08:44:00+00:00'], ContextMetadataDummy::class, null, [
ObjectNormalizer::GROUPS => 'extended',
]);
self::assertEquals(new \DateTime('2011-07-28T08:44:00+00:00'), $dummy->date, 'base denormalization context is unchanged for this group');
/** @var ContextMetadataDummy $dummy */
$dummy = $normalizer->denormalize(['date' => '28/07/2011'], ContextMetadataDummy::class, null, [
ObjectNormalizer::GROUPS => 'simple',
]);
self::assertEquals('2011-07-28', $dummy->date->format('Y-m-d'), 'a specific denormalization context is used for this group');
}
}
class ContextMetadataDummy
{
/**
* @var \DateTime
*
* @Groups({ "extended", "simple" })
* @Context({ DateTimeNormalizer::FORMAT_KEY = \DateTime::RFC3339 })
* @Context(
* normalizationContext = { DateTimeNormalizer::FORMAT_KEY = \DateTime::RFC3339_EXTENDED },
* groups = {"extended"}
* )
* @Context(
* denormalizationContext = { DateTimeNormalizer::FORMAT_KEY = "d/m/Y" },
* groups = {"simple"}
* )
*/
public $date;
}

View File

@ -42,6 +42,7 @@ use Symfony\Component\Serializer\Tests\Normalizer\Features\AttributesTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CircularReferenceTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\ConstructorArgumentsTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\ContextMetadataTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\GroupsTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\IgnoredAttributesTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\MaxDepthTestTrait;
@ -59,6 +60,7 @@ class ObjectNormalizerTest extends TestCase
use CallbacksTestTrait;
use CircularReferenceTestTrait;
use ConstructorArgumentsTestTrait;
use ContextMetadataTestTrait;
use GroupsTestTrait;
use IgnoredAttributesTestTrait;
use MaxDepthTestTrait;

View File

@ -37,6 +37,7 @@
"symfony/property-info": "^5.3",
"symfony/uid": "^5.1",
"symfony/validator": "^4.4|^5.0",
"symfony/var-dumper": "^4.4|^5.0",
"symfony/var-exporter": "^4.4|^5.0",
"symfony/yaml": "^4.4|^5.0"
},