AbstractNormalizer.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  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\Serializer\Exception\CircularReferenceException;
  12. use Symfony\Component\Serializer\Exception\InvalidArgumentException;
  13. use Symfony\Component\Serializer\Exception\LogicException;
  14. use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
  15. use Symfony\Component\Serializer\Exception\RuntimeException;
  16. use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
  17. use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
  18. use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
  19. use Symfony\Component\Serializer\SerializerAwareInterface;
  20. use Symfony\Component\Serializer\SerializerAwareTrait;
  21. /**
  22. * Normalizer implementation.
  23. *
  24. * @author Kévin Dunglas <dunglas@gmail.com>
  25. */
  26. abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
  27. {
  28. use ObjectToPopulateTrait;
  29. use SerializerAwareTrait;
  30. /* constants to configure the context */
  31. /**
  32. * How many loops of circular reference to allow while normalizing.
  33. *
  34. * The default value of 1 means that when we encounter the same object a
  35. * second time, we consider that a circular reference.
  36. *
  37. * You can raise this value for special cases, e.g. in combination with the
  38. * max depth setting of the object normalizer.
  39. */
  40. public const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
  41. /**
  42. * Instead of creating a new instance of an object, update the specified object.
  43. *
  44. * If you have a nested structure, child objects will be overwritten with
  45. * new instances unless you set DEEP_OBJECT_TO_POPULATE to true.
  46. */
  47. public const OBJECT_TO_POPULATE = 'object_to_populate';
  48. /**
  49. * Only (de)normalize attributes that are in the specified groups.
  50. */
  51. public const GROUPS = 'groups';
  52. /**
  53. * Limit (de)normalize to the specified names.
  54. *
  55. * For nested structures, this list needs to reflect the object tree.
  56. */
  57. public const ATTRIBUTES = 'attributes';
  58. /**
  59. * If ATTRIBUTES are specified, and the source has fields that are not part of that list,
  60. * either ignore those attributes (true) or throw an ExtraAttributesException (false).
  61. */
  62. public const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes';
  63. /**
  64. * Hashmap of default values for constructor arguments.
  65. *
  66. * The names need to match the parameter names in the constructor arguments.
  67. */
  68. public const DEFAULT_CONSTRUCTOR_ARGUMENTS = 'default_constructor_arguments';
  69. /**
  70. * Hashmap of field name => callable to normalize this field.
  71. *
  72. * The callable is called if the field is encountered with the arguments:
  73. *
  74. * - mixed $attributeValue value of this field
  75. * - object $object the whole object being normalized
  76. * - string $attributeName name of the attribute being normalized
  77. * - string $format the requested format
  78. * - array $context the serialization context
  79. */
  80. public const CALLBACKS = 'callbacks';
  81. /**
  82. * Handler to call when a circular reference has been detected.
  83. *
  84. * If you specify no handler, a CircularReferenceException is thrown.
  85. *
  86. * The method will be called with ($object, $format, $context) and its
  87. * return value is returned as the result of the normalize call.
  88. */
  89. public const CIRCULAR_REFERENCE_HANDLER = 'circular_reference_handler';
  90. /**
  91. * Skip the specified attributes when normalizing an object tree.
  92. *
  93. * This list is applied to each element of nested structures.
  94. *
  95. * Note: The behaviour for nested structures is different from ATTRIBUTES
  96. * for historical reason. Aligning the behaviour would be a BC break.
  97. */
  98. public const IGNORED_ATTRIBUTES = 'ignored_attributes';
  99. /**
  100. * @internal
  101. */
  102. protected const CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'circular_reference_limit_counters';
  103. protected $defaultContext = [
  104. self::ALLOW_EXTRA_ATTRIBUTES => true,
  105. self::CIRCULAR_REFERENCE_HANDLER => null,
  106. self::CIRCULAR_REFERENCE_LIMIT => 1,
  107. self::IGNORED_ATTRIBUTES => [],
  108. ];
  109. /**
  110. * @var ClassMetadataFactoryInterface|null
  111. */
  112. protected $classMetadataFactory;
  113. /**
  114. * @var NameConverterInterface|null
  115. */
  116. protected $nameConverter;
  117. /**
  118. * Sets the {@link ClassMetadataFactoryInterface} to use.
  119. */
  120. public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, array $defaultContext = [])
  121. {
  122. $this->classMetadataFactory = $classMetadataFactory;
  123. $this->nameConverter = $nameConverter;
  124. $this->defaultContext = array_merge($this->defaultContext, $defaultContext);
  125. if (isset($this->defaultContext[self::CALLBACKS])) {
  126. if (!\is_array($this->defaultContext[self::CALLBACKS])) {
  127. throw new InvalidArgumentException(sprintf('The "%s" default context option must be an array of callables.', self::CALLBACKS));
  128. }
  129. foreach ($this->defaultContext[self::CALLBACKS] as $attribute => $callback) {
  130. if (!\is_callable($callback)) {
  131. throw new InvalidArgumentException(sprintf('Invalid callback found for attribute "%s" in the "%s" default context option.', $attribute, self::CALLBACKS));
  132. }
  133. }
  134. }
  135. if (isset($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER]) && !\is_callable($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER])) {
  136. throw new InvalidArgumentException(sprintf('Invalid callback found in the "%s" default context option.', self::CIRCULAR_REFERENCE_HANDLER));
  137. }
  138. }
  139. /**
  140. * {@inheritdoc}
  141. */
  142. public function hasCacheableSupportsMethod(): bool
  143. {
  144. return false;
  145. }
  146. /**
  147. * Detects if the configured circular reference limit is reached.
  148. *
  149. * @return bool
  150. *
  151. * @throws CircularReferenceException
  152. */
  153. protected function isCircularReference(object $object, array &$context)
  154. {
  155. $objectHash = spl_object_hash($object);
  156. $circularReferenceLimit = $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT];
  157. if (isset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash])) {
  158. if ($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] >= $circularReferenceLimit) {
  159. unset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]);
  160. return true;
  161. }
  162. ++$context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash];
  163. } else {
  164. $context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] = 1;
  165. }
  166. return false;
  167. }
  168. /**
  169. * Handles a circular reference.
  170. *
  171. * If a circular reference handler is set, it will be called. Otherwise, a
  172. * {@class CircularReferenceException} will be thrown.
  173. *
  174. * @final
  175. *
  176. * @return mixed
  177. *
  178. * @throws CircularReferenceException
  179. */
  180. protected function handleCircularReference(object $object, string $format = null, array $context = [])
  181. {
  182. $circularReferenceHandler = $context[self::CIRCULAR_REFERENCE_HANDLER] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER];
  183. if ($circularReferenceHandler) {
  184. return $circularReferenceHandler($object, $format, $context);
  185. }
  186. throw new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d).', get_debug_type($object), $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT]));
  187. }
  188. /**
  189. * Gets attributes to normalize using groups.
  190. *
  191. * @param string|object $classOrObject
  192. * @param bool $attributesAsString If false, return an array of {@link AttributeMetadataInterface}
  193. *
  194. * @throws LogicException if the 'allow_extra_attributes' context variable is false and no class metadata factory is provided
  195. *
  196. * @return string[]|AttributeMetadataInterface[]|bool
  197. */
  198. protected function getAllowedAttributes($classOrObject, array $context, bool $attributesAsString = false)
  199. {
  200. $allowExtraAttributes = $context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES];
  201. if (!$this->classMetadataFactory) {
  202. if (!$allowExtraAttributes) {
  203. throw new LogicException(sprintf('A class metadata factory must be provided in the constructor when setting "%s" to false.', self::ALLOW_EXTRA_ATTRIBUTES));
  204. }
  205. return false;
  206. }
  207. $tmpGroups = $context[self::GROUPS] ?? $this->defaultContext[self::GROUPS] ?? null;
  208. $groups = (\is_array($tmpGroups) || is_scalar($tmpGroups)) ? (array) $tmpGroups : false;
  209. $allowedAttributes = [];
  210. $ignoreUsed = false;
  211. foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) {
  212. if ($ignore = $attributeMetadata->isIgnored()) {
  213. $ignoreUsed = true;
  214. }
  215. // If you update this check, update accordingly the one in Symfony\Component\PropertyInfo\Extractor\SerializerExtractor::getProperties()
  216. if (
  217. !$ignore &&
  218. (false === $groups || array_intersect(array_merge($attributeMetadata->getGroups(), ['*']), $groups)) &&
  219. $this->isAllowedAttribute($classOrObject, $name = $attributeMetadata->getName(), null, $context)
  220. ) {
  221. $allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata;
  222. }
  223. }
  224. if (!$ignoreUsed && false === $groups && $allowExtraAttributes) {
  225. // Backward Compatibility with the code using this method written before the introduction of @Ignore
  226. return false;
  227. }
  228. return $allowedAttributes;
  229. }
  230. /**
  231. * Is this attribute allowed?
  232. *
  233. * @param object|string $classOrObject
  234. *
  235. * @return bool
  236. */
  237. protected function isAllowedAttribute($classOrObject, string $attribute, string $format = null, array $context = [])
  238. {
  239. $ignoredAttributes = $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES];
  240. if (\in_array($attribute, $ignoredAttributes)) {
  241. return false;
  242. }
  243. $attributes = $context[self::ATTRIBUTES] ?? $this->defaultContext[self::ATTRIBUTES] ?? null;
  244. if (isset($attributes[$attribute])) {
  245. // Nested attributes
  246. return true;
  247. }
  248. if (\is_array($attributes)) {
  249. return \in_array($attribute, $attributes, true);
  250. }
  251. return true;
  252. }
  253. /**
  254. * Normalizes the given data to an array. It's particularly useful during
  255. * the denormalization process.
  256. *
  257. * @param object|array $data
  258. *
  259. * @return array
  260. */
  261. protected function prepareForDenormalization($data)
  262. {
  263. return (array) $data;
  264. }
  265. /**
  266. * Returns the method to use to construct an object. This method must be either
  267. * the object constructor or static.
  268. *
  269. * @param array|bool $allowedAttributes
  270. *
  271. * @return \ReflectionMethod|null
  272. */
  273. protected function getConstructor(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes)
  274. {
  275. return $reflectionClass->getConstructor();
  276. }
  277. /**
  278. * Instantiates an object using constructor parameters when needed.
  279. *
  280. * This method also allows to denormalize data into an existing object if
  281. * it is present in the context with the object_to_populate. This object
  282. * is removed from the context before being returned to avoid side effects
  283. * when recursively normalizing an object graph.
  284. *
  285. * @param array|bool $allowedAttributes
  286. *
  287. * @return object
  288. *
  289. * @throws RuntimeException
  290. * @throws MissingConstructorArgumentsException
  291. */
  292. protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
  293. {
  294. if (null !== $object = $this->extractObjectToPopulate($class, $context, self::OBJECT_TO_POPULATE)) {
  295. unset($context[self::OBJECT_TO_POPULATE]);
  296. return $object;
  297. }
  298. // clean up even if no match
  299. unset($context[static::OBJECT_TO_POPULATE]);
  300. $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
  301. if ($constructor) {
  302. if (true !== $constructor->isPublic()) {
  303. return $reflectionClass->newInstanceWithoutConstructor();
  304. }
  305. $constructorParameters = $constructor->getParameters();
  306. $params = [];
  307. foreach ($constructorParameters as $constructorParameter) {
  308. $paramName = $constructorParameter->name;
  309. $key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
  310. $allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes);
  311. $ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
  312. if ($constructorParameter->isVariadic()) {
  313. if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
  314. if (!\is_array($data[$paramName])) {
  315. throw new RuntimeException(sprintf('Cannot create an instance of "%s" from serialized data because the variadic parameter "%s" can only accept an array.', $class, $constructorParameter->name));
  316. }
  317. $variadicParameters = [];
  318. foreach ($data[$paramName] as $parameterData) {
  319. $variadicParameters[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format);
  320. }
  321. $params = array_merge($params, $variadicParameters);
  322. unset($data[$key]);
  323. }
  324. } elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
  325. $parameterData = $data[$key];
  326. if (null === $parameterData && $constructorParameter->allowsNull()) {
  327. $params[] = null;
  328. // Don't run set for a parameter passed to the constructor
  329. unset($data[$key]);
  330. continue;
  331. }
  332. // Don't run set for a parameter passed to the constructor
  333. $params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format);
  334. unset($data[$key]);
  335. } elseif (\array_key_exists($key, $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
  336. $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
  337. } elseif (\array_key_exists($key, $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
  338. $params[] = $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
  339. } elseif ($constructorParameter->isDefaultValueAvailable()) {
  340. $params[] = $constructorParameter->getDefaultValue();
  341. } else {
  342. throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name));
  343. }
  344. }
  345. if ($constructor->isConstructor()) {
  346. return $reflectionClass->newInstanceArgs($params);
  347. } else {
  348. return $constructor->invokeArgs(null, $params);
  349. }
  350. }
  351. return new $class();
  352. }
  353. /**
  354. * @internal
  355. */
  356. protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, string $parameterName, $parameterData, array $context, string $format = null)
  357. {
  358. try {
  359. if (($parameterType = $parameter->getType()) instanceof \ReflectionNamedType && !$parameterType->isBuiltin()) {
  360. $parameterClass = $parameterType->getName();
  361. new \ReflectionClass($parameterClass); // throws a \ReflectionException if the class doesn't exist
  362. if (!$this->serializer instanceof DenormalizerInterface) {
  363. 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));
  364. }
  365. return $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName, $format));
  366. }
  367. } catch (\ReflectionException $e) {
  368. throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $parameterName), 0, $e);
  369. } catch (MissingConstructorArgumentsException $e) {
  370. if (!$parameter->getType()->allowsNull()) {
  371. throw $e;
  372. }
  373. return null;
  374. }
  375. return $parameterData;
  376. }
  377. /**
  378. * @internal
  379. */
  380. protected function createChildContext(array $parentContext, string $attribute, ?string $format): array
  381. {
  382. if (isset($parentContext[self::ATTRIBUTES][$attribute])) {
  383. $parentContext[self::ATTRIBUTES] = $parentContext[self::ATTRIBUTES][$attribute];
  384. } else {
  385. unset($parentContext[self::ATTRIBUTES]);
  386. }
  387. return $parentContext;
  388. }
  389. }