8000 [Serializer] Allow to provide (de)normalization context in mapping · hultberg/symfony@7229fa1 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7229fa1

Browse files
ogizanagifabpot
authored andcommitted
[Serializer] Allow to provide (de)normalization context in mapping
1 parent d9f490a commit 7229fa1

27 files changed

+1183
-17
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Annotation;
13+
14+
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
15+
16+
/**
17+
* Annotation class for @Context().
18+
*
19+
* @Annotation
20+
* @Target({"PROPERTY", "METHOD"})
21+
*
22+
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
23+
*/
24+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
25+
final class Context
26+
{
27+
private $context;
28+
private $normalizationContext;
29+
private $denormalizationContext;
30+
private $groups;
31+
32+
/**
33+
* @throws InvalidArgumentException
34+
*/
35+
public function __construct(array $options = [], array $context = [], array $normalizationContext = [], array $denormalizationContext = [], array $groups = [])
36+
{
37+
if (!$context) {
38+
if (!array_intersect((array_keys($options)), ['normalizationContext', 'groups', 'context', 'value', 'denormalizationContext'])) {
39+
// gracefully supports context as first, unnamed attribute argument if it cannot be confused with Doctrine-style options
40+
$context = $options;
41+
} else {
42+
// If at least one of the options match, it's likely to be Doctrine-style options. Search for the context inside:
43+
$context = $options['value'] ?? $options['context'] ?? [];
44+
}
45+
}
46+
47+
$normalizationContext = $options['normalizationContext'] ?? $normalizationContext;
48+
$denormalizationContext = $options['denormalizationContext'] ?? $denormalizationContext;
49+
50+
foreach (compact(['context', 'normalizationContext', 'denormalizationContext']) as $key => $value) {
51+
if (!\is_array($value)) {
52+
throw new InvalidArgumentException(sprintf('Option "%s" of annotation "%s" must be an array.', $key, static::class));
53+
}
54+
}
55+
56+
if (!$context && !$normalizationContext && !$denormalizationContext) {
57+
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));
58+
}
59+
60+
$groups = (array) ($options['groups'] ?? $groups);
61+
62+
foreach ($groups as $group) {
63+
if (!\is_string($group)) {
64+
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)));
65+
}
66+
}
67+
68+
$this->context = $context;
69+
$this->normalizationContext = $normalizationContext;
70+
$this->denormalizationContext = $denormalizationContext;
71+
$this->groups = $groups;
72+
}
73+
74+
public function getContext(): array
75+
{
76+
return $this->context;
77+
}
78+
79+
public function getNormalizationContext(): array
80+
{
81+
return $this->normalizationContext;
82+
}
83+
84+
public function getDenormalizationContext(): array
85+
{
86+
return $this->denormalizationContext;
87+
}
88+
89+
public function getGroups(): array
90+
{
91+
return $this->groups;
92+
}
93+
}

src/Symfony/Component/Serializer/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
5.3
55
---
66

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

src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,24 @@ class AttributeMetadata implements AttributeMetadataInterface
5959
*/
6060
public $ignore = false;
6161

62+
/**
63+
* @var array[] Normalization contexts per group name ("*" applies to all groups)
64+
*
65+
* @internal This property is public in order to reduce the size of the
66+
* class' serialized representation. Do not access it. Use
67+
* {@link getNormalizationContexts()} instead.
68+
*/
69+
public $normalizationContexts = [];
70+
71+
/**
72+
* @var array[] Denormalization contexts per group name ("*" applies to all groups)
73+
*
74+
* @internal This property is public in order to reduce the size of the
75+
* class' serialized representation. Do not access it. Use
76+
* {@link getDenormalizationContexts()} instead.
77+
*/
78+
public $denormalizationContexts = [];
79+
6280
public function __construct(string $name)
6381
{
6482
$this->name = $name;
@@ -138,6 +156,76 @@ public function isIgnored(): bool
138156
return $this->ignore;
139157
}
140158

159+
/**
160+
* {@inheritdoc}
161+
*/
162+
public function getNormalizationContexts(): array
163+
{
164+
return $this->normalizationContexts;
165+
}
166+
167+
/**
168+
* {@inheritdoc}
169+
*/
170+
public function getNormalizationContextForGroups(array $groups): array
171+
{
172+
$contexts = [];
173+
foreach ($groups as $group) {
174+
$contexts[] = $this->normalizationContexts[$group] ?? [];
175+
}
176+
177+
return array_merge($this->normalizationContexts['*'] ?? [], ...$contexts);
178+
}
179+
180+
/**
181+
* {@inheritdoc}
182+
*/
183+
public function setNormalizationContextForGroups(array $context, array $groups = []): void
184+
{
185+
if (!$groups) {
186+
$this->normalizationContexts['*'] = $context;
187+
}
188+
189+
foreach ($groups as $group) {
190+
$this->normalizationContexts[$group] = $context;
191+
}
192+
}
193+
194+
/**
195+
* {@inheritdoc}
196+
*/
197+
public function getDenormalizationContexts(): array
198+
{
199+
return $this->denormalizationContexts;
200+
}
201+
202+
/**
203+
* {@inheritdoc}
204+
*/
205+
public function getDenormalizationContextForGroups(array $groups): array
206+
{
207+
$contexts = [];
208+
foreach ($groups as $group) {
209+
$contexts[] = $this->denormalizationContexts[$group] ?? [];
210+
}
211+
212+
return array_merge($this->denormalizationContexts['*'] ?? [], ...$contexts);
213+
}
214+
215+
/**
216+
* {@inheritdoc}
217+
*/
218+
public function setDenormalizationContextForGroups(array $context, array $groups = []): void
219+
{
220+
if (!$groups) {
221+
$this->denormalizationContexts['*'] = $context;
222+
}
223+
224+
foreach ($groups as $group) {
225+
$this->denormalizationContexts[$group] = $context;
226+
}
227+
}
228+
141229
/**
142230
* {@inheritdoc}
143231
*/
@@ -157,6 +245,12 @@ public function merge(AttributeMetadataInterface $attributeMetadata)
157245
$this->serializedName = $attributeMetadata->getSerializedName();
158246
}
159247

248+
// Overwrite only if both contexts are empty
249+
if (!$this->normalizationContexts && !$this->denormalizationContexts) {
250+
$this->normalizationContexts = $attributeMetadata->getNormalizationContexts();
251+
$this->denormalizationContexts = $attributeMetadata->getDenormalizationContexts();
252+
}
253+
160254
if ($ignore = $attributeMetadata->isIgnored()) {
161255
$this->ignore = $ignore;
162256
}
@@ -169,6 +263,6 @@ public function merge(AttributeMetadataInterface $attributeMetadata)
169263
*/
170264< F438 /code>
public function __sleep()
171265
{
172-
return ['name', 'groups', 'maxDepth', 'serializedName', 'ignore'];
266+
return ['name', 'groups', 'maxDepth', 'serializedName', 'ignore', 'normalizationContexts', 'denormalizationContexts'];
173267
}
174268
}

src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,34 @@ public function isIgnored(): bool;
7575
* Merges an {@see AttributeMetadataInterface} with in the current one.
7676
*/
7777
public function merge(self $attributeMetadata);
78+
79+
/**
80+
* Gets all the normalization contexts per group ("*" being the base context applied to all groups).
81+
*/
82+
public function getNormalizationContexts(): array;
83+
84+
/**
85+
* Gets the computed normalization contexts for given groups.
86+
*/
87+
public function getNormalizationContextForGroups(array $groups): array;
88+
89+
/**
90+
* Sets the normalization context for given groups.
91+
*/
92+
public function setNormalizationContextForGroups(array $context, array $groups = []): void;
93+
94+
/**
95+
* Gets all the denormalization contexts per group ("*" being the base context applied to all groups).
96+
*/
97+
public function getDenormalizationContexts(): array;
98+
99+
/**
100+
* Gets the computed denormalization contexts for given groups.
101+
*/
102+
public function getDenormalizationContextForGroups(array $groups): array;
103+
104+
/**
105+
* Sets the denormalization context for given groups.
106+
*/
107+
public function setDenormalizationContextForGroups(array $context, array $groups = []): void;
78108
}

src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
namespace Symfony\Component\Serializer\Mapping\Loader;
1313

1414
use Doctrine\Common\Annotations\Reader;
15+
use Symfony\Component\Serializer\Annotation\Context;
1516
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
1617
use Symfony\Component\Serializer\Annotation\Groups;
1718
use Symfony\Component\Serializer\Annotation\Ignore;
1819
use Symfony\Component\Serializer\Annotation\MaxDepth;
1920
use Symfony\Component\Serializer\Annotation\SerializedName;
2021
use Symfony\Component\Serializer\Exception\MappingException;
2122
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
23+
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
2224
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
2325
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
2426

@@ -36,6 +38,7 @@ class AnnotationLoader implements LoaderInterface
3638
Ignore::class => true,
3739
MaxDepth::class => true,
3840
SerializedName::class => true,
41+
Context::class => true,
3942
];
4043

4144
private $reader;
@@ -83,6 +86,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
8386
$attributesMetadata[$property->name]->setSerializedName($annotation->getSerializedName());
8487
} elseif ($annotation instanceof Ignore) {
8588
$attributesMetadata[$property->name]->setIgnore(true);
89+
} elseif ($annotation instanceof Context) {
90+
$this->setAttributeContextsForGroups($annotation, $attributesMetadata[$property->name]);
8691
}
8792

8893
$loaded = true;
@@ -130,6 +135,12 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
130135
$attributeMetadata->setSerializedName($annotation->getSerializedName());
131136
} elseif ($annotation instanceof Ignore) {
132137
$attributeMetadata->setIgnore(true);
138+
} elseif ($annotation instanceof Context) {
139+
if (!$accessorOrMutator) {
140+
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));
141+
}
142+
143+
$this->setAttributeContextsForGroups($annotation, $attributeMetadata);
133144
}
134145

135146
$loaded = true;
@@ -166,4 +177,20 @@ public function loadAnnotations(object $reflector): iterable
166177
yield from $this->reader->getPropertyAnnotations($reflector);
167178
}
168179
}
180+
181+
private function setAttributeContextsForGroups(Context $annotation, AttributeMetadataInterface $attributeMetadata): void
182+
{
183+
if ($annotation->getContext()) {
184+
$attributeMetadata->setNormalizationContextForGroups($annotation->getContext(), $annotation->getGroups());
185+
$attributeMetadata->setDenormalizationContextForGroups($annotation->getContext(), $annotation->getGroups());
186+
}
187+
188+
if ($annotation->getNormalizationContext()) {
189+
$attributeMetadata->setNormalizationContextForGroups($annotation->getNormalizationContext(), $annotation->getGroups());
190+
}
191+
192+
if ($annotation->getDenormalizationContext()) {
193+
$attributeMetadata->setDenormalizationContextForGroups($annotation->getDenormalizationContext(), $annotation->getGroups());
194+
}
195+
}
169196
}

src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,25 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
7474
if (isset($attribute['ignore'])) {
7575
$attributeMetadata->setIgnore((bool) $attribute['ignore']);
7676
}
77+
78+
foreach ($attribute->context as $node) {
79+
$groups = (array) $node->group;
80+
$context = $this->parseContext($node->entry);
81+
$attributeMetadata->setNormalizationContextForGroups($context, $groups);
82+
$attributeMetadata->setDenormalizationContextForGroups($context, $groups);
83+
}
84+
85+
foreach ($attribute->normalization_context as $node) {
86+
$groups = (array) $node->group;
87+
$context = $this->parseContext($node->entry);
88+
$attributeMetadata->setNormalizationContextForGroups($context, $groups);
89+
}
90+
91+
foreach ($attribute->denormalization_context as $node) {
92+
$groups = (array) $node->group;
93+
$context = $this->parseContext($node->entry);
94+
$attributeMetadata->setDenormalizationContextForGroups($context, $groups);
95+
}
7796
}
7897

7998
if (isset($xml->{'discriminator-map'})) {
@@ -136,4 +155,29 @@ private function getClassesFromXml(): array
136155

137156
return $classes;
138157
}
158+
159+
private function parseContext(\SimpleXMLElement $nodes): array
160+
{
161+
$context = [];
162+
163+
foreach ($nodes as $node) {
164+
if (\count($node) > 0) {
165+
if (\count($node->entry) > 0) {
166+
$value = $this->parseContext($node->entry);
167+
} else {
168+
$value = [];
169+
}
170+
} else {
171+
$value = XmlUtils::phpize($node);
172+
}
173+
174+
if (isset($node['name'])) {
175+
$context[(string) $node['name']] = $value;
176+
} else {
177+
$context[] = $value;
178+
}
179+
}
180+
181+
return $context;
182+
}
139183
}

0 commit comments

Comments
 (0)
0