8000 bug #45323 [Serializer] Fix ignored callbacks in denormalization (ben… · symfony/symfony@7fd9127 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7fd9127

Browse files
bug #45323 [Serializer] Fix ignored callbacks in denormalization (benjaminmal)
This PR was squashed before being merged into the 4.4 branch. Discussion ---------- [Serializer] Fix ignored callbacks in denormalization | Q | A | ------------- | --- | Branch? | 4.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | Fix #43563-2 | License | MIT | Doc PR | - Stop ignoring callbacks in denormalization. #freeTheCallbacks!!! Commits ------- abe18a0 [Serializer] Fix ignored callbacks in denormalization
2 parents a6aecb4 + abe18a0 commit 7fd9127

File tree

7 files changed

+299
-56
lines changed

7 files changed

+299
-56
lines changed

src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,15 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
7979
public const DEFAULT_CONSTRUCTOR_ARGUMENTS = 'default_constructor_arguments';
8080

8181
/**
82-
* Hashmap of field name => callable to normalize this field.
82+
* Hashmap of field name => callable to (de)normalize this field.
8383
*
8484
* The callable is called if the field is encountered with the arguments:
8585
*
86-
* - mixed $attributeValue value of this field
87-
* - object $object the whole object being normalized
88-
* - string $attributeName name of the attribute being normalized
89-
* - string $format the requested format
90-
* - array $context the serialization context
86+
* - mixed $attributeValue value of this field
87+
* - object|string $object the whole object being normalized or the object's class being denormalized
88+
* - string $attributeName name of the attribute being (de)normalized
89+
* - string $format the requested format
90+
* - array $context the serialization context
9191
*/
9292
public const CALLBACKS = 'callbacks';
9393

@@ -168,17 +168,7 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory
168168
$this->nameConverter = $nameConverter;
169169
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
170170

171-
if (isset($this->defaultContext[self::CALLBACKS])) {
172-
if (!\is_array($this->defaultContext[self::CALLBACKS])) {
173-
throw new InvalidArgumentException(sprintf('The "%s" default context option must be an array of callables.', self::CALLBACKS));
174-
}
175-
176-
foreach ($this->defaultContext[self::CALLBACKS] as $attribute => $callback) {
177-
if (!\is_callable($callback)) {
178-
throw new InvalidArgumentException(sprintf('Invalid callback found for attribute "%s" in the "%s" default context option.', $attribute, self::CALLBACKS));
179-
}
180-
}
181-
}
171+
$this->validateCallbackContext($this->defaultContext, 'default');
182172

183173
if (isset($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER]) && !\is_callable($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER])) {
184174
throw new InvalidArgumentException(sprintf('Invalid callback found in the "%s" default context option.', self::CIRCULAR_REFERENCE_HANDLER));
@@ -220,11 +210,11 @@ public function setCircularReferenceHandler(callable $circularReferenceHandler)
220210
}
221211

222212
/**
223-
* Sets normalization callbacks.
213+
* Sets (de)normalization callbacks.
224214
*
225215
* @deprecated since Symfony 4.2
226216
*
227-
* @param callable[] $callbacks Help normalize the result
217+
* @param callable[] $callbacks Help (de)normalize the result
228218
*
229219
* @return self
230220
*
@@ -532,7 +522,7 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara
532522
throw new LogicException(sprintf('Cannot create an instance of "%s" from serialized data because the serializer inject in "%s" is not a denormalizer.', $parameterClass, static::class));
533523
}
534524

535-
return $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName, $format));
525+
$parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName, $format));
536526
}
537527
} catch (\ReflectionException $e) {
538528
throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $parameterName), 0, $e);
@@ -544,7 +534,7 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara
544534
return null;
545535
}
546536

547-
return $parameterData;
537+
return $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context);
548538
}
549539

550540
/**
@@ -565,4 +555,46 @@ protected function createChildContext(array $parentContext, $attribute/*, ?strin
565555

566556
return $parentContext;
567557
}
558+
559+
/**
560+
* Validate callbacks set in context.
561+
*
562+
* @param string $contextType Used to specify which context is invalid in exceptions
563+
*
564+
* @throws InvalidArgumentException
565+
*/
566+
final protected function validateCallbackContext(array $context, string $contextType = ''): void
567+
{
568+
if (!isset($context[self::CALLBACKS])) {
569+
return;
570+
}
571+
572+
if (!\is_array($context[self::CALLBACKS])) {
573+
throw new InvalidArgumentException(sprintf('The "%s"%s context option must be an array of callables.', self::CALLBACKS, '' !== $contextType ? " $contextType" : ''));
574+
}
575+
576+
foreach ($context[self::CALLBACKS] as $attribute => $callback) {
577+
if (!\is_callable($callback)) {
578+
throw new InvalidArgumentException(sprintf('Invalid callback found for attribute "%s" in the "%s"%s context option.', $attribute, self::CALLBACKS, '' !== $contextType ? " $contextType" : ''));
579+
}
580+
}
581+
}
582+
583+
/**
584+
* Apply callbacks set in context.
585+
*
586+
* @param mixed $value
587+
* @param object|string $object Can be either the object being normalizing or the object's class being denormalized
588+
*
589+
* @return mixed
590+
*/
591+
final protected function applyCallbacks($value, $object, string $attribute, ?string $format, array $context)
592+
{
593+
/**
594+
* @var callable|null
595+
*/
596+
$callback = $context[self::CALLBACKS][$attribute] ?? $this->defaultContext[self::CALLBACKS][$attribute] ?? $this->callbacks[$attribute] ?? null;
597+
598+
return $callback ? $callback($value, $object, $attribute, $format, $context) : $value;
599+
}
568600
}

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -145,17 +145,7 @@ public function normalize($object, $format = null, array $context = [])
145145
$context['cache_key'] = $this->getCacheKey($format, $context);
146146
}
147147

148-
if (isset($context[self::CALLBACKS])) {
149-
if (!\is_array($context[self::CALLBACKS])) {
150-
throw new InvalidArgumentException(sprintf('The "%s" context option must be an array of callables.', self::CALLBACKS));
151-
}
152-
153-
foreach ($context[self::CALLBACKS] as $attribute => $callback) {
154-
if (!\is_callable($callback)) {
155-
throw new InvalidArgumentException(sprintf('Invalid callback found for attribute "%s" in the "%s" context option.', $attribute, self::CALLBACKS));
156-
}
157-
}
158-
}
148+
$this->validateCallbackContext($context);
159149

160150
if ($this->isCircularReference($object, $context)) {
161151
return $this->handleCircularReference($object, $format, $context);
@@ -203,13 +193,7 @@ public function normalize($object, $format = null, array $context = [])
203193
$attributeValue = $maxDepthHandler($attributeValue, < 10000 span class=pl-s1>$object, $attribute, $format, $context);
204194
}
205195

206-
/**
207-
* @var callable|null
208-
*/
209-
$callback = $context[self::CALLBACKS][$attribute] ?? $this->defaultContext[self::CALLBACKS][$attribute] ?? $this->callbacks[$attribute] ?? null;
210-
if ($callback) {
211-
$attributeValue = $callback($attributeValue, $object, $attribute, $format, $context);
212-
}
196+
$attributeValue = $this->applyCallbacks($attributeValue, $object, $attribute, $format, $context);
213197

214198
if (null !== $attributeValue && !is_scalar($attributeValue)) {
215199
$stack[$attribute] = $attributeValue;
@@ -346,6 +330,8 @@ public function denormalize($data, $type, $format = null, array $context = [])
346330
$context['cache_key'] = $this->getCacheKey($format, $context);
347331
}
348332

333+
$this->validateCallbackContext($context);
334+
349335
$allowedAttributes = $this->getAllowedAttributes($type, $context, true);
350336
$normalizedData = $this->prepareForDenormalization($data);
351337
$extraAttributes = [];
@@ -375,6 +361,8 @@ public function denormalize($data, $type, $format = null, array $context = [])
375361
}
376362

377363
$value = $this->validateAndDenormalize($resolvedClass, $attribute, $value, $format, $context);
364+
$value = $this->applyCallbacks($value, $resolvedClass, $attribute, $format, $context);
365+
378366
try {
379367
$this->setAttributeValue($object, $attribute, $value, $format, $context);
380368
} catch (InvalidArgumentException $e) {
@@ -509,7 +497,9 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara
509497
return parent::denormalizeParameter($class, $parameter, $parameterName, $parameterData, $context, $format);
510498
}
511499

512-
return $this->validateAndDenormalize($class->getName(), $parameterName, $parameterData, $format, $context);
500+
$parameterData = $this->validateAndDenormalize($class->getName(), $parameterName, $parameterData, $format, $context);
501+
502+
return $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context);
513503
}
514504

515505
/**

src/Symfony/Component/Serializer/Tests/Normalizer/Features/CallbacksObject.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,34 @@ class CallbacksObject
66
{
77
public $bar;
88

9-
public function __construct($bar = null)
9+
/**
10+
* @var string|null
11+
*/
12+
public $foo;
13+
14+
public function __construct($bar = null, string $foo = null)
1015
{
1116
$this->bar = $bar;
17+
$this->foo = $foo;
1218
}
1319

1420
public function getBar()
1521
{
1622
return $this->bar;
1723
}
24+
25+
public function setBar($bar)
26+
{
27+
$this->bar = $bar;
28+
}
29+
30+
public function getFoo(): ?string
31+
{
32+
return $this->foo;
33+
}
34+
35+
public function setFoo(?string $foo)
36+
{
37+
$this->foo = $foo;
38+
}
1839
}

0 commit comments

Comments
 (0)
0