feature #39399 [Serializer] Allow to provide (de)normalization context in mapping (ogizanagi)

This PR was squashed before being merged into the 5.3-dev branch.

Discussion
----------

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

| Q             | A
| ------------- | ---
| Branch?       | 5.x
| Bug fix?      | no
| New feature?  | yes <!-- please update src/**/CHANGELOG.md files -->
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tickets       | Fix #39039 <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License       | MIT
| Doc PR        | TODO <!-- required for new features -->

As explained in the linked feature request, this brings the ability to configure context on a per-property basis, using Serializer mapping.

Considering:

```php
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

class Foo
{
    /**
     * @Serializer\Context({ DateTimeNormalizer::FORMAT_KEY = 'Y-m-d' })
     */
    public \DateTime $date;

    public \DateTime $anotherDate;
}
```

`$date` will be formatted with a specific format, while `$anotherDate` will use the default configured one (or the one provided in the context while calling `->serialize()` / `->normalize()`).

It can also differentiate normalization and denormalization contexts:

```php
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

class Foo
{
    /**
     * @Serializer\Context(
     *   normalizationContext = { DateTimeNormalizer::FORMAT_KEY = 'Y-m-d' },
     *   denormalizationContext = { DateTimeNormalizer::FORMAT_KEY = \DateTime::COOKIE },
     * )
     */
    public \DateTime $date;
}
```

As well as act differently depending on groups:

```php
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

class Foo
{
    /**
     * @Serializer\Groups({ "extended" })
     * @Serializer\Context({ DateTimeNormalizer::FORMAT_KEY = \DateTime::RFC3339 })
     * @Serializer\Context(
     *   context = { DateTimeNormalizer::FORMAT_KEY = \DateTime::RFC3339_EXTENDED },
     *   groups = {"extended"},
     * )
     */
    public \DateTime $date;
}
```

The annotation can be repeated as much as you want to handle the different cases.
Context without groups is always applied first, then context for groups are merged in the provided order.
Context provided when calling `->serialize()` / `->normalize()` acts as the defaults for the properties without context provided in the metadata.

XML mapping (see tests) is a lot verbose due to the required structure to handle groups.

Such metadata contexts are also forwarded to name converters, max depth handlers, callbacks, ...

Of course, PHP 8 attributes are also supported:

```php
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;

class Foo
{
    #[Serializer\Groups(["extended"])]
    #[Serializer\Context([DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339])]
    #[Serializer\Context(
      context: [DateTimeNormalizer::FORMAT_KEY => \DateTime::RFC3339_EXTENDED],
      groups: ["extended"],
    )]
    public \DateTime $date;
}
```

The PR should be ready for first batch of reviews / discussions.

- [x] Make Fabbot happy in 5.2
- [x] Missing `@Context` unit tests
- [x] rework xml & phpize values
- [x] Fix lowest build issue with annotations => bumped doctrine annotations to 1.7, as for other components

Commits
-------

7229fa1d8f [Serializer] Allow to provide (de)normalization context in mapping
This commit is contained in:
Fabien Potencier 2021-02-16 07:56:07 +01:00
commit e2b1d9cd5a
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"
},