AbstractNormalizer.php 10 KB

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