[Serializer] Allow to provide (de)normalization context in mapping
This commit is contained in:
parent
d9f490a8b2
commit
7229fa1d8f
93
src/Symfony/Component/Serializer/Annotation/Context.php
Normal file
93
src/Symfony/Component/Serializer/Annotation/Context.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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`
|
||||
|
||||
|
@ -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'];
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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?
|
||||
*
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
||||
|
@ -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 }
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
@ -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"
|
||||
},
|
||||
|
Reference in New Issue
Block a user