AbstractObjectNormalizer.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Serializer\Normalizer;
  11. use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
  12. use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
  13. use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
  14. use Symfony\Component\PropertyInfo\Type;
  15. use Symfony\Component\Serializer\Encoder\CsvEncoder;
  16. use Symfony\Component\Serializer\Encoder\JsonEncoder;
  17. use Symfony\Component\Serializer\Encoder\XmlEncoder;
  18. use Symfony\Component\Serializer\Exception\ExtraAttributesException;
  19. use Symfony\Component\Serializer\Exception\LogicException;
  20. use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
  21. use Symfony\Component\Serializer\Exception\RuntimeException;
  22. use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
  23. use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
  24. use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
  25. use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
  26. use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
  27. /**
  28. * Base class for a normalizer dealing with objects.
  29. *
  30. * @author Kévin Dunglas <dunglas@gmail.com>
  31. */
  32. abstract class AbstractObjectNormalizer extends AbstractNormalizer
  33. {
  34. /**
  35. * Set to true to respect the max depth metadata on fields.
  36. */
  37. public const ENABLE_MAX_DEPTH = 'enable_max_depth';
  38. /**
  39. * How to track the current depth in the context.
  40. */
  41. public const DEPTH_KEY_PATTERN = 'depth_%s::%s';
  42. /**
  43. * While denormalizing, we can verify that types match.
  44. *
  45. * You can disable this by setting this flag to true.
  46. */
  47. public const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement';
  48. /**
  49. * Flag to control whether fields with the value `null` should be output
  50. * when normalizing or omitted.
  51. */
  52. public const SKIP_NULL_VALUES = 'skip_null_values';
  53. /**
  54. * Callback to allow to set a value for an attribute when the max depth has
  55. * been reached.
  56. *
  57. * If no callback is given, the attribute is skipped. If a callable is
  58. * given, its return value is used (even if null).
  59. *
  60. * The arguments are:
  61. *
  62. * - mixed $attributeValue value of this field
  63. * - object $object the whole object being normalized
  64. * - string $attributeName name of the attribute being normalized
  65. * - string $format the requested format
  66. * - array $context the serialization context
  67. */
  68. public const MAX_DEPTH_HANDLER = 'max_depth_handler';
  69. /**
  70. * Specify which context key are not relevant to determine which attributes
  71. * of an object to (de)normalize.
  72. */
  73. public const EXCLUDE_FROM_CACHE_KEY = 'exclude_from_cache_key';
  74. /**
  75. * Flag to tell the denormalizer to also populate existing objects on
  76. * attributes of the main object.
  77. *
  78. * Setting this to true is only useful if you also specify the root object
  79. * in OBJECT_TO_POPULATE.
  80. */
  81. public const DEEP_OBJECT_TO_POPULATE = 'deep_object_to_populate';
  82. public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects';
  83. private $propertyTypeExtractor;
  84. private $typesCache = [];
  85. private $attributesCache = [];
  86. private $objectClassResolver;
  87. /**
  88. * @var ClassDiscriminatorResolverInterface|null
  89. */
  90. protected $classDiscriminatorResolver;
  91. public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = [])
  92. {
  93. parent::__construct($classMetadataFactory, $nameConverter, $defaultContext);
  94. if (isset($this->defaultContext[self::MAX_DEPTH_HANDLER]) && !\is_callable($this->defaultContext[self::MAX_DEPTH_HANDLER])) {
  95. throw new InvalidArgumentException(sprintf('The "%s" given in the default context is not callable.', self::MAX_DEPTH_HANDLER));
  96. }
  97. $this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] = array_merge($this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] ?? [], [self::CIRCULAR_REFERENCE_LIMIT_COUNTERS]);
  98. $this->propertyTypeExtractor = $propertyTypeExtractor;
  99. if (null === $classDiscriminatorResolver && null !== $classMetadataFactory) {
  100. $classDiscriminatorResolver = new ClassDiscriminatorFromClassMetadata($classMetadataFactory);
  101. }
  102. $this->classDiscriminatorResolver = $classDiscriminatorResolver;
  103. $this->objectClassResolver = $objectClassResolver;
  104. }
  105. /**
  106. * {@inheritdoc}
  107. */
  108. public function supportsNormalization($data, string $format = null)
  109. {
  110. return \is_object($data) && !$data instanceof \Traversable;
  111. }
  112. /**
  113. * {@inheritdoc}
  114. */
  115. public function normalize($object, string $format = null, array $context = [])
  116. {
  117. if (!isset($context['cache_key'])) {
  118. $context['cache_key'] = $this->getCacheKey($format, $context);
  119. }
  120. if (isset($context[self::CALLBACKS])) {
  121. if (!\is_array($context[self::CALLBACKS])) {
  122. throw new InvalidArgumentException(sprintf('The "%s" context option must be an array of callables.', self::CALLBACKS));
  123. }
  124. foreach ($context[self::CALLBACKS] as $attribute => $callback) {
  125. if (!\is_callable($callback)) {
  126. throw new InvalidArgumentException(sprintf('Invalid callback found for attribute "%s" in the "%s" context option.', $attribute, self::CALLBACKS));
  127. }
  128. }
  129. }
  130. if ($this->isCircularReference($object, $context)) {
  131. return $this->handleCircularReference($object, $format, $context);
  132. }
  133. $data = [];
  134. $stack = [];
  135. $attributes = $this->getAttributes($object, $format, $context);
  136. $class = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
  137. $attributesMetadata = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
  138. if (isset($context[self::MAX_DEPTH_HANDLER])) {
  139. $maxDepthHandler = $context[self::MAX_DEPTH_HANDLER];
  140. if (!\is_callable($maxDepthHandler)) {
  141. throw new InvalidArgumentException(sprintf('The "%s" given in the context is not callable.', self::MAX_DEPTH_HANDLER));
  142. }
  143. } else {
  144. $maxDepthHandler = null;
  145. }
  146. foreach ($attributes as $attribute) {
  147. $maxDepthReached = false;
  148. if (null !== $attributesMetadata && ($maxDepthReached = $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) && !$maxDepthHandler) {
  149. continue;
  150. }
  151. $attributeValue = $this->getAttributeValue($object, $attribute, $format, $context);
  152. if ($maxDepthReached) {
  153. $attributeValue = $maxDepthHandler($attributeValue, $object, $attribute, $format, $context);
  154. }
  155. /**
  156. * @var callable|null
  157. */
  158. $callback = $context[self::CALLBACKS][$attribute] ?? $this->defaultContext[self::CALLBACKS][$attribute] ?? null;
  159. if ($callback) {
  160. $attributeValue = $callback($attributeValue, $object, $attribute, $format, $context);
  161. }
  162. if (null !== $attributeValue && !is_scalar($attributeValue)) {
  163. $stack[$attribute] = $attributeValue;
  164. }
  165. $data = $this->updateData($data, $attribute, $attributeValue, $class, $format, $context);
  166. }
  167. foreach ($stack as $attribute => $attributeValue) {
  168. if (!$this->serializer instanceof NormalizerInterface) {
  169. throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer.', $attribute));
  170. }
  171. $data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute, $format)), $class, $format, $context);
  172. }
  173. if (isset($context[self::PRESERVE_EMPTY_OBJECTS]) && !\count($data)) {
  174. return new \ArrayObject();
  175. }
  176. return $data;
  177. }
  178. /**
  179. * {@inheritdoc}
  180. */
  181. protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
  182. {
  183. if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
  184. if (!isset($data[$mapping->getTypeProperty()])) {
  185. throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class));
  186. }
  187. $type = $data[$mapping->getTypeProperty()];
  188. if (null === ($mappedClass = $mapping->getClassForType($type))) {
  189. throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s".', $type, $class));
  190. }
  191. if ($mappedClass !== $class) {
  192. return $this->instantiateObject($data, $mappedClass, $context, new \ReflectionClass($mappedClass), $allowedAttributes, $format);
  193. }
  194. }
  195. return parent::instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes, $format);
  196. }
  197. /**
  198. * Gets and caches attributes for the given object, format and context.
  199. *
  200. * @param object $object
  201. *
  202. * @return string[]
  203. */
  204. protected function getAttributes($object, ?string $format, array $context)
  205. {
  206. $class = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
  207. $key = $class.'-'.$context['cache_key'];
  208. if (isset($this->attributesCache[$key])) {
  209. return $this->attributesCache[$key];
  210. }
  211. $allowedAttributes = $this->getAllowedAttributes($object, $context, true);
  212. if (false !== $allowedAttributes) {
  213. if ($context['cache_key']) {
  214. $this->attributesCache[$key] = $allowedAttributes;
  215. }
  216. return $allowedAttributes;
  217. }
  218. $attributes = $this->extractAttributes($object, $format, $context);
  219. if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object)) {
  220. array_unshift($attributes, $mapping->getTypeProperty());
  221. }
  222. if ($context['cache_key'] && \stdClass::class !== $class) {
  223. $this->attributesCache[$key] = $attributes;
  224. }
  225. return $attributes;
  226. }
  227. /**
  228. * Extracts attributes to normalize from the class of the given object, format and context.
  229. *
  230. * @return string[]
  231. */
  232. abstract protected function extractAttributes(object $object, string $format = null, array $context = []);
  233. /**
  234. * Gets the attribute value.
  235. *
  236. * @return mixed
  237. */
  238. abstract protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []);
  239. /**
  240. * {@inheritdoc}
  241. */
  242. public function supportsDenormalization($data, string $type, string $format = null)
  243. {
  244. return class_exists($type) || (interface_exists($type, false) && $this->classDiscriminatorResolver && null !== $this->classDiscriminatorResolver->getMappingForClass($type));
  245. }
  246. /**
  247. * {@inheritdoc}
  248. */
  249. public function denormalize($data, string $type, string $format = null, array $context = [])
  250. {
  251. if (!isset($context['cache_key'])) {
  252. $context['cache_key'] = $this->getCacheKey($format, $context);
  253. }
  254. $allowedAttributes = $this->getAllowedAttributes($type, $context, true);
  255. $normalizedData = $this->prepareForDenormalization($data);
  256. $extraAttributes = [];
  257. $reflectionClass = new \ReflectionClass($type);
  258. $object = $this->instantiateObject($normalizedData, $type, $context, $reflectionClass, $allowedAttributes, $format);
  259. $resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
  260. foreach ($normalizedData as $attribute => $value) {
  261. if ($this->nameConverter) {
  262. $attribute = $this->nameConverter->denormalize($attribute, $resolvedClass, $format, $context);
  263. }
  264. if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($resolvedClass, $attribute, $format, $context)) {
  265. if (!($context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES])) {
  266. $extraAttributes[] = $attribute;
  267. }
  268. continue;
  269. }
  270. if ($context[self::DEEP_OBJECT_TO_POPULATE] ?? $this->defaultContext[self::DEEP_OBJECT_TO_POPULATE] ?? false) {
  271. try {
  272. $context[self::OBJECT_TO_POPULATE] = $this->getAttributeValue($object, $attribute, $format, $context);
  273. } catch (NoSuchPropertyException $e) {
  274. }
  275. }
  276. $value = $this->validateAndDenormalize($resolvedClass, $attribute, $value, $format, $context);
  277. try {
  278. $this->setAttributeValue($object, $attribute, $value, $format, $context);
  279. } catch (InvalidArgumentException $e) {
  280. throw new NotNormalizableValueException(sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type), $e->getCode(), $e);
  281. }
  282. }
  283. if (!empty($extraAttributes)) {
  284. throw new ExtraAttributesException($extraAttributes);
  285. }
  286. return $object;
  287. }
  288. /**
  289. * Sets attribute value.
  290. */
  291. abstract protected function setAttributeValue(object $object, string $attribute, $value, string $format = null, array $context = []);
  292. /**
  293. * Validates the submitted data and denormalizes it.
  294. *
  295. * @param mixed $data
  296. *
  297. * @return mixed
  298. *
  299. * @throws NotNormalizableValueException
  300. * @throws LogicException
  301. */
  302. private function validateAndDenormalize(string $currentClass, string $attribute, $data, ?string $format, array $context)
  303. {
  304. if (null === $types = $this->getTypes($currentClass, $attribute)) {
  305. return $data;
  306. }
  307. $expectedTypes = [];
  308. foreach ($types as $type) {
  309. if (null === $data && $type->isNullable()) {
  310. return null;
  311. }
  312. $collectionValueType = $type->isCollection() ? $type->getCollectionValueType() : null;
  313. // Fix a collection that contains the only one element
  314. // This is special to xml format only
  315. if ('xml' === $format && null !== $collectionValueType && (!\is_array($data) || !\is_int(key($data)))) {
  316. $data = [$data];
  317. }
  318. // In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine,
  319. // if a value is meant to be a string, float, int or a boolean value from the serialized representation.
  320. // That's why we have to transform the values, if one of these non-string basic datatypes is expected.
  321. if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) {
  322. if ('' === $data && $type->isNullable() && \in_array($type->getBuiltinType(), [Type::BUILTIN_TYPE_BOOL, Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_FLOAT], true)) {
  323. return null;
  324. }
  325. switch ($type->getBuiltinType()) {
  326. case Type::BUILTIN_TYPE_BOOL:
  327. // according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
  328. if ('false' === $data || '0' === $data) {
  329. $data = false;
  330. } elseif ('true' === $data || '1' === $data) {
  331. $data = true;
  332. } else {
  333. throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data));
  334. }
  335. break;
  336. case Type::BUILTIN_TYPE_INT:
  337. if (ctype_digit($data) || '-' === $data[0] && ctype_digit(substr($data, 1))) {
  338. $data = (int) $data;
  339. } else {
  340. throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data));
  341. }
  342. break;
  343. case Type::BUILTIN_TYPE_FLOAT:
  344. if (is_numeric($data)) {
  345. return (float) $data;
  346. }
  347. switch ($data) {
  348. case 'NaN':
  349. return \NAN;
  350. case 'INF':
  351. return \INF;
  352. case '-INF':
  353. return -\INF;
  354. default:
  355. throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data));
  356. }
  357. break;
  358. }
  359. }
  360. if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
  361. $builtinType = Type::BUILTIN_TYPE_OBJECT;
  362. $class = $collectionValueType->getClassName().'[]';
  363. if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
  364. $context['key_type'] = $collectionKeyType;
  365. }
  366. } elseif ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_ARRAY === $collectionValueType->getBuiltinType()) {
  367. // get inner type for any nested array
  368. $innerType = $collectionValueType;
  369. // note that it will break for any other builtinType
  370. $dimensions = '[]';
  371. while (null !== $innerType->getCollectionValueType() && Type::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) {
  372. $dimensions .= '[]';
  373. $innerType = $innerType->getCollectionValueType();
  374. }
  375. if (null !== $innerType->getClassName()) {
  376. // the builtinType is the inner one and the class is the class followed by []...[]
  377. $builtinType = $innerType->getBuiltinType();
  378. $class = $innerType->getClassName().$dimensions;
  379. } else {
  380. // default fallback (keep it as array)
  381. $builtinType = $type->getBuiltinType();
  382. $class = $type->getClassName();
  383. }
  384. } else {
  385. $builtinType = $type->getBuiltinType();
  386. $class = $type->getClassName();
  387. }
  388. $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true;
  389. if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
  390. if (!$this->serializer instanceof DenormalizerInterface) {
  391. throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.', $attribute, $class));
  392. }
  393. $childContext = $this->createChildContext($context, $attribute, $format);
  394. if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) {
  395. return $this->serializer->denormalize($data, $class, $format, $childContext);
  396. }
  397. }
  398. // JSON only has a Number type corresponding to both int and float PHP types.
  399. // PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
  400. // floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
  401. // PHP's json_decode automatically converts Numbers without a decimal part to integers.
  402. // To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
  403. // a float is expected.
  404. if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && false !== strpos($format, JsonEncoder::FORMAT)) {
  405. return (float) $data;
  406. }
  407. if (('is_'.$builtinType)($data)) {
  408. return $data;
  409. }
  410. }
  411. if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) {
  412. return $data;
  413. }
  414. throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), get_debug_type($data)));
  415. }
  416. /**
  417. * @internal
  418. */
  419. protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, string $parameterName, $parameterData, array $context, string $format = null)
  420. {
  421. if ($parameter->isVariadic() || null === $this->propertyTypeExtractor || null === $this->propertyTypeExtractor->getTypes($class->getName(), $parameterName)) {
  422. return parent::denormalizeParameter($class, $parameter, $parameterName, $parameterData, $context, $format);
  423. }
  424. return $this->validateAndDenormalize($class->getName(), $parameterName, $parameterData, $format, $context);
  425. }
  426. /**
  427. * @return Type[]|null
  428. */
  429. private function getTypes(string $currentClass, string $attribute): ?array
  430. {
  431. if (null === $this->propertyTypeExtractor) {
  432. return null;
  433. }
  434. $key = $currentClass.'::'.$attribute;
  435. if (isset($this->typesCache[$key])) {
  436. return false === $this->typesCache[$key] ? null : $this->typesCache[$key];
  437. }
  438. if (null !== $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) {
  439. return $this->typesCache[$key] = $types;
  440. }
  441. if (null !== $this->classDiscriminatorResolver && null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($currentClass)) {
  442. if ($discriminatorMapping->getTypeProperty() === $attribute) {
  443. return $this->typesCache[$key] = [
  444. new Type(Type::BUILTIN_TYPE_STRING),
  445. ];
  446. }
  447. foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
  448. if (null !== $types = $this->propertyTypeExtractor->getTypes($mappedClass, $attribute)) {
  449. return $this->typesCache[$key] = $types;
  450. }
  451. }
  452. }
  453. $this->typesCache[$key] = false;
  454. return null;
  455. }
  456. /**
  457. * Sets an attribute and apply the name converter if necessary.
  458. *
  459. * @param mixed $attributeValue
  460. */
  461. private function updateData(array $data, string $attribute, $attributeValue, string $class, ?string $format, array $context): array
  462. {
  463. if (null === $attributeValue && ($context[self::SKIP_NULL_VALUES] ?? $this->defaultContext[self::SKIP_NULL_VALUES] ?? false)) {
  464. return $data;
  465. }
  466. if ($this->nameConverter) {
  467. $attribute = $this->nameConverter->normalize($attribute, $class, $format, $context);
  468. }
  469. $data[$attribute] = $attributeValue;
  470. return $data;
  471. }
  472. /**
  473. * Is the max depth reached for the given attribute?
  474. *
  475. * @param AttributeMetadataInterface[] $attributesMetadata
  476. */
  477. private function isMaxDepthReached(array $attributesMetadata, string $class, string $attribute, array &$context): bool
  478. {
  479. $enableMaxDepth = $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false;
  480. if (
  481. !$enableMaxDepth ||
  482. !isset($attributesMetadata[$attribute]) ||
  483. null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth()
  484. ) {
  485. return false;
  486. }
  487. $key = sprintf(self::DEPTH_KEY_PATTERN, $class, $attribute);
  488. if (!isset($context[$key])) {
  489. $context[$key] = 1;
  490. return false;
  491. }
  492. if ($context[$key] === $maxDepth) {
  493. return true;
  494. }
  495. ++$context[$key];
  496. return false;
  497. }
  498. /**
  499. * Overwritten to update the cache key for the child.
  500. *
  501. * We must not mix up the attribute cache between parent and children.
  502. *
  503. * {@inheritdoc}
  504. *
  505. * @internal
  506. */
  507. protected function createChildContext(array $parentContext, string $attribute, ?string $format): array
  508. {
  509. $context = parent::createChildContext($parentContext, $attribute, $format);
  510. $context['cache_key'] = $this->getCacheKey($format, $context);
  511. return $context;
  512. }
  513. /**
  514. * Builds the cache key for the attributes cache.
  515. *
  516. * The key must be different for every option in the context that could change which attributes should be handled.
  517. *
  518. * @return bool|string
  519. */
  520. private function getCacheKey(?string $format, array $context)
  521. {
  522. foreach ($context[self::EXCLUDE_FROM_CACHE_KEY] ?? $this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] as $key) {
  523. unset($context[$key]);
  524. }
  525. unset($context[self::EXCLUDE_FROM_CACHE_KEY]);
  526. unset($context[self::OBJECT_TO_POPULATE]);
  527. unset($context['cache_key']); // avoid artificially different keys
  528. try {
  529. return md5($format.serialize([
  530. 'context' => $context,
  531. 'ignored' => $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES],
  532. ]));
  533. } catch (\Exception $exception) {
  534. // The context cannot be serialized, skip the cache
  535. return false;
  536. }
  537. }
  538. }