AbstractNormalizer.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  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\RuntimeException;
  15. use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
  16. use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
  17. use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
  18. use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
  19. /**
  20. * Normalizer implementation.
  21. *
  22. * @author Kévin Dunglas <dunglas@gmail.com>
  23. */
  24. abstract class AbstractNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface
  25. {
  26. const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
  27. const OBJECT_TO_POPULATE = 'object_to_populate';
  28. const GROUPS = 'groups';
  29. /**
  30. * @var int
  31. */
  32. protected $circularReferenceLimit = 1;
  33. /**
  34. * @var callable
  35. */
  36. protected $circularReferenceHandler;
  37. /**
  38. * @var ClassMetadataFactoryInterface|null
  39. */
  40. protected $classMetadataFactory;
  41. /**
  42. * @var NameConverterInterface|null
  43. */
  44. protected $nameConverter;
  45. /**
  46. * @var array
  47. */
  48. protected $callbacks = array();
  49. /**
  50. * @var array
  51. */
  52. protected $ignoredAttributes = array();
  53. /**
  54. * @var array
  55. */
  56. protected $camelizedAttributes = array();
  57. /**
  58. * Sets the {@link ClassMetadataFactoryInterface} to use.
  59. *
  60. * @param ClassMetadataFactoryInterface|null $classMetadataFactory
  61. * @param NameConverterInterface|null $nameConverter
  62. */
  63. public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null)
  64. {
  65. $this->classMetadataFactory = $classMetadataFactory;
  66. $this->nameConverter = $nameConverter;
  67. }
  68. /**
  69. * Set circular reference limit.
  70. *
  71. * @param int $circularReferenceLimit limit of iterations for the same object
  72. *
  73. * @return self
  74. */
  75. public function setCircularReferenceLimit($circularReferenceLimit)
  76. {
  77. $this->circularReferenceLimit = $circularReferenceLimit;
  78. return $this;
  79. }
  80. /**
  81. * Set circular reference handler.
  82. *
  83. * @param callable $circularReferenceHandler
  84. *
  85. * @return self
  86. *
  87. * @throws InvalidArgumentException
  88. */
  89. public function setCircularReferenceHandler($circularReferenceHandler)
  90. {
  91. if (!is_callable($circularReferenceHandler)) {
  92. throw new InvalidArgumentException('The given circular reference handler is not callable.');
  93. }
  94. $this->circularReferenceHandler = $circularReferenceHandler;
  95. return $this;
  96. }
  97. /**
  98. * Set normalization callbacks.
  99. *
  100. * @param callable[] $callbacks help normalize the result
  101. *
  102. * @return self
  103. *
  104. * @throws InvalidArgumentException if a non-callable callback is set
  105. */
  106. public function setCallbacks(array $callbacks)
  107. {
  108. foreach ($callbacks as $attribute => $callback) {
  109. if (!is_callable($callback)) {
  110. throw new InvalidArgumentException(sprintf(
  111. 'The given callback for attribute "%s" is not callable.',
  112. $attribute
  113. ));
  114. }
  115. }
  116. $this->callbacks = $callbacks;
  117. return $this;
  118. }
  119. /**
  120. * Set ignored attributes for normalization and denormalization.
  121. *
  122. * @param array $ignoredAttributes
  123. *
  124. * @return self
  125. */
  126. public function setIgnoredAttributes(array $ignoredAttributes)
  127. {
  128. $this->ignoredAttributes = $ignoredAttributes;
  129. return $this;
  130. }
  131. /**
  132. * Set attributes to be camelized on denormalize.
  133. *
  134. * @deprecated Deprecated since version 2.7, to be removed in 3.0. Use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter instead.
  135. *
  136. * @param array $camelizedAttributes
  137. *
  138. * @return self
  139. *
  140. * @throws LogicException
  141. */
  142. public function setCamelizedAttributes(array $camelizedAttributes)
  143. {
  144. @trigger_error(sprintf('%s is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter instead.', __METHOD__), E_USER_DEPRECATED);
  145. if ($this->nameConverter && !$this->nameConverter instanceof CamelCaseToSnakeCaseNameConverter) {
  146. throw new LogicException(sprintf('%s cannot be called if a custom Name Converter is defined.', __METHOD__));
  147. }
  148. $attributes = array();
  149. foreach ($camelizedAttributes as $camelizedAttribute) {
  150. $attributes[] = lcfirst(preg_replace_callback('/(^|_|\.)+(.)/', function ($match) {
  151. return ('.' === $match[1] ? '_' : '').strtoupper($match[2]);
  152. }, $camelizedAttribute));
  153. }
  154. $this->nameConverter = new CamelCaseToSnakeCaseNameConverter($attributes);
  155. return $this;
  156. }
  157. /**
  158. * Detects if the configured circular reference limit is reached.
  159. *
  160. * @param object $object
  161. * @param array $context
  162. *
  163. * @return bool
  164. *
  165. * @throws CircularReferenceException
  166. */
  167. protected function isCircularReference($object, &$context)
  168. {
  169. $objectHash = spl_object_hash($object);
  170. if (isset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash])) {
  171. if ($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] >= $this->circularReferenceLimit) {
  172. unset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash]);
  173. return true;
  174. }
  175. ++$context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash];
  176. } else {
  177. $context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] = 1;
  178. }
  179. return false;
  180. }
  181. /**
  182. * Handles a circular reference.
  183. *
  184. * If a circular reference handler is set, it will be called. Otherwise, a
  185. * {@class CircularReferenceException} will be thrown.
  186. *
  187. * @param object $object
  188. *
  189. * @return mixed
  190. *
  191. * @throws CircularReferenceException
  192. */
  193. protected function handleCircularReference($object)
  194. {
  195. if ($this->circularReferenceHandler) {
  196. return call_user_func($this->circularReferenceHandler, $object);
  197. }
  198. throw new CircularReferenceException(sprintf('A circular reference has been detected (configured limit: %d).', $this->circularReferenceLimit));
  199. }
  200. /**
  201. * Format an attribute name, for example to convert a snake_case name to camelCase.
  202. *
  203. * @deprecated Deprecated since version 2.7, to be removed in 3.0. Use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter instead.
  204. *
  205. * @param string $attributeName
  206. *
  207. * @return string
  208. */
  209. protected function formatAttribute($attributeName)
  210. {
  211. @trigger_error(sprintf('%s is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter instead.', __METHOD__), E_USER_DEPRECATED);
  212. return $this->nameConverter ? $this->nameConverter->normalize($attributeName) : $attributeName;
  213. }
  214. /**
  215. * Gets attributes to normalize using groups.
  216. *
  217. * @param string|object $classOrObject
  218. * @param array $context
  219. * @param bool $attributesAsString If false, return an array of {@link AttributeMetadataInterface}
  220. *
  221. * @return string[]|AttributeMetadataInterface[]|bool
  222. */
  223. protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
  224. {
  225. if (!$this->classMetadataFactory || !isset($context[static::GROUPS]) || !is_array($context[static::GROUPS])) {
  226. return false;
  227. }
  228. $allowedAttributes = array();
  229. foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) {
  230. if (count(array_intersect($attributeMetadata->getGroups(), $context[static::GROUPS]))) {
  231. $allowedAttributes[] = $attributesAsString ? $attributeMetadata->getName() : $attributeMetadata;
  232. }
  233. }
  234. return $allowedAttributes;
  235. }
  236. /**
  237. * Normalizes the given data to an array. It's particularly useful during
  238. * the denormalization process.
  239. *
  240. * @param object|array $data
  241. *
  242. * @return array
  243. */
  244. protected function prepareForDenormalization($data)
  245. {
  246. return (array) $data;
  247. }
  248. /**
  249. * Instantiates an object using constructor parameters when needed.
  250. *
  251. * This method also allows to denormalize data into an existing object if
  252. * it is present in the context with the object_to_populate. This object
  253. * is removed from the context before being returned to avoid side effects
  254. * when recursively normalizing an object graph.
  255. *
  256. * @param array $data
  257. * @param string $class
  258. * @param array $context
  259. * @param \ReflectionClass $reflectionClass
  260. * @param array|bool $allowedAttributes
  261. *
  262. * @return object
  263. *
  264. * @throws RuntimeException
  265. */
  266. protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes)
  267. {
  268. if (
  269. isset($context[static::OBJECT_TO_POPULATE]) &&
  270. is_object($context[static::OBJECT_TO_POPULATE]) &&
  271. $context[static::OBJECT_TO_POPULATE] instanceof $class
  272. ) {
  273. $object = $context[static::OBJECT_TO_POPULATE];
  274. unset($context[static::OBJECT_TO_POPULATE]);
  275. return $object;
  276. }
  277. $constructor = $reflectionClass->getConstructor();
  278. if ($constructor) {
  279. $constructorParameters = $constructor->getParameters();
  280. $params = array();
  281. foreach ($constructorParameters as $constructorParameter) {
  282. $paramName = $constructorParameter->name;
  283. $key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName;
  284. $allowed = $allowedAttributes === false || in_array($paramName, $allowedAttributes);
  285. $ignored = in_array($paramName, $this->ignoredAttributes);
  286. if (method_exists($constructorParameter, 'isVariadic') && $constructorParameter->isVariadic()) {
  287. if ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) {
  288. if (!is_array($data[$paramName])) {
  289. 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));
  290. }
  291. $params = array_merge($params, $data[$paramName]);
  292. }
  293. } elseif ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) {
  294. $params[] = $data[$key];
  295. // don't run set for a parameter passed to the constructor
  296. unset($data[$key]);
  297. } elseif ($constructorParameter->isDefaultValueAvailable()) {
  298. $params[] = $constructorParameter->getDefaultValue();
  299. } else {
  300. throw new RuntimeException(
  301. sprintf(
  302. 'Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.',
  303. $class,
  304. $constructorParameter->name
  305. )
  306. );
  307. }
  308. }
  309. return $reflectionClass->newInstanceArgs($params);
  310. }
  311. return new $class();
  312. }
  313. }