ClassMetadata.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  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\Validator\Mapping;
  11. use Symfony\Component\Validator\Constraint;
  12. use Symfony\Component\Validator\Constraints\Cascade;
  13. use Symfony\Component\Validator\Constraints\Composite;
  14. use Symfony\Component\Validator\Constraints\GroupSequence;
  15. use Symfony\Component\Validator\Constraints\Traverse;
  16. use Symfony\Component\Validator\Constraints\Valid;
  17. use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
  18. use Symfony\Component\Validator\Exception\GroupDefinitionException;
  19. /**
  20. * Default implementation of {@link ClassMetadataInterface}.
  21. *
  22. * This class supports serialization and cloning.
  23. *
  24. * @author Bernhard Schussek <bschussek@gmail.com>
  25. * @author Fabien Potencier <fabien@symfony.com>
  26. */
  27. class ClassMetadata extends GenericMetadata implements ClassMetadataInterface
  28. {
  29. /**
  30. * @var string
  31. *
  32. * @internal This property is public in order to reduce the size of the
  33. * class' serialized representation. Do not access it. Use
  34. * {@link getClassName()} instead.
  35. */
  36. public $name;
  37. /**
  38. * @var string
  39. *
  40. * @internal This property is public in order to reduce the size of the
  41. * class' serialized representation. Do not access it. Use
  42. * {@link getDefaultGroup()} instead.
  43. */
  44. public $defaultGroup;
  45. /**
  46. * @var MemberMetadata[][]
  47. *
  48. * @internal This property is public in order to reduce the size of the
  49. * class' serialized representation. Do not access it. Use
  50. * {@link getPropertyMetadata()} instead.
  51. */
  52. public $members = [];
  53. /**
  54. * @var PropertyMetadata[]
  55. *
  56. * @internal This property is public in order to reduce the size of the
  57. * class' serialized representation. Do not access it. Use
  58. * {@link getPropertyMetadata()} instead.
  59. */
  60. public $properties = [];
  61. /**
  62. * @var GetterMetadata[]
  63. *
  64. * @internal This property is public in order to reduce the size of the
  65. * class' serialized representation. Do not access it. Use
  66. * {@link getPropertyMetadata()} instead.
  67. */
  68. public $getters = [];
  69. /**
  70. * @var array
  71. *
  72. * @internal This property is public in order to reduce the size of the
  73. * class' serialized representation. Do not access it. Use
  74. * {@link getGroupSequence()} instead.
  75. */
  76. public $groupSequence = [];
  77. /**
  78. * @var bool
  79. *
  80. * @internal This property is public in order to reduce the size of the
  81. * class' serialized representation. Do not access it. Use
  82. * {@link isGroupSequenceProvider()} instead.
  83. */
  84. public $groupSequenceProvider = false;
  85. /**
  86. * The strategy for traversing traversable objects.
  87. *
  88. * By default, only instances of {@link \Traversable} are traversed.
  89. *
  90. * @var int
  91. *
  92. * @internal This property is public in order to reduce the size of the
  93. * class' serialized representation. Do not access it. Use
  94. * {@link getTraversalStrategy()} instead.
  95. */
  96. public $traversalStrategy = TraversalStrategy::IMPLICIT;
  97. /**
  98. * @var \ReflectionClass
  99. */
  100. private $reflClass;
  101. public function __construct(string $class)
  102. {
  103. $this->name = $class;
  104. // class name without namespace
  105. if (false !== $nsSep = strrpos($class, '\\')) {
  106. $this->defaultGroup = substr($class, $nsSep + 1);
  107. } else {
  108. $this->defaultGroup = $class;
  109. }
  110. }
  111. /**
  112. * {@inheritdoc}
  113. */
  114. public function __sleep()
  115. {
  116. $parentProperties = parent::__sleep();
  117. // Don't store the cascading strategy. Classes never cascade.
  118. unset($parentProperties[array_search('cascadingStrategy', $parentProperties)]);
  119. return array_merge($parentProperties, [
  120. 'getters',
  121. 'groupSequence',
  122. 'groupSequenceProvider',
  123. 'members',
  124. 'name',
  125. 'properties',
  126. 'defaultGroup',
  127. ]);
  128. }
  129. /**
  130. * {@inheritdoc}
  131. */
  132. public function getClassName()
  133. {
  134. return $this->name;
  135. }
  136. /**
  137. * Returns the name of the default group for this class.
  138. *
  139. * For each class, the group "Default" is an alias for the group
  140. * "<ClassName>", where <ClassName> is the non-namespaced name of the
  141. * class. All constraints implicitly or explicitly assigned to group
  142. * "Default" belong to both of these groups, unless the class defines
  143. * a group sequence.
  144. *
  145. * If a class defines a group sequence, validating the class in "Default"
  146. * will validate the group sequence. The constraints assigned to "Default"
  147. * can still be validated by validating the class in "<ClassName>".
  148. *
  149. * @return string The name of the default group
  150. */
  151. public function getDefaultGroup()
  152. {
  153. return $this->defaultGroup;
  154. }
  155. /**
  156. * {@inheritdoc}
  157. *
  158. * If the constraint {@link Cascade} is added, the cascading strategy will be
  159. * changed to {@link CascadingStrategy::CASCADE}.
  160. *
  161. * If the constraint {@link Traverse} is added, the traversal strategy will be
  162. * changed. Depending on the $traverse property of that constraint,
  163. * the traversal strategy will be set to one of the following:
  164. *
  165. * - {@link TraversalStrategy::IMPLICIT} by default
  166. * - {@link TraversalStrategy::NONE} if $traverse is disabled
  167. * - {@link TraversalStrategy::TRAVERSE} if $traverse is enabled
  168. */
  169. public function addConstraint(Constraint $constraint)
  170. {
  171. $this->checkConstraint($constraint);
  172. if ($constraint instanceof Traverse) {
  173. if ($constraint->traverse) {
  174. // If traverse is true, traversal should be explicitly enabled
  175. $this->traversalStrategy = TraversalStrategy::TRAVERSE;
  176. } else {
  177. // If traverse is false, traversal should be explicitly disabled
  178. $this->traversalStrategy = TraversalStrategy::NONE;
  179. }
  180. // The constraint is not added
  181. return $this;
  182. }
  183. if ($constraint instanceof Cascade) {
  184. if (\PHP_VERSION_ID < 70400) {
  185. throw new ConstraintDefinitionException(sprintf('The constraint "%s" requires PHP 7.4.', Cascade::class));
  186. }
  187. $this->cascadingStrategy = CascadingStrategy::CASCADE;
  188. foreach ($this->getReflectionClass()->getProperties() as $property) {
  189. if ($property->hasType() && (('array' === $type = $property->getType()->getName()) || class_exists(($type)))) {
  190. $this->addPropertyConstraint($property->getName(), new Valid());
  191. }
  192. }
  193. // The constraint is not added
  194. return $this;
  195. }
  196. $constraint->addImplicitGroupName($this->getDefaultGroup());
  197. parent::addConstraint($constraint);
  198. return $this;
  199. }
  200. /**
  201. * Adds a constraint to the given property.
  202. *
  203. * @return $this
  204. */
  205. public function addPropertyConstraint(string $property, Constraint $constraint)
  206. {
  207. if (!isset($this->properties[$property])) {
  208. $this->properties[$property] = new PropertyMetadata($this->getClassName(), $property);
  209. $this->addPropertyMetadata($this->properties[$property]);
  210. }
  211. $constraint->addImplicitGroupName($this->getDefaultGroup());
  212. $this->properties[$property]->addConstraint($constraint);
  213. return $this;
  214. }
  215. /**
  216. * @param Constraint[] $constraints
  217. *
  218. * @return $this
  219. */
  220. public function addPropertyConstraints(string $property, array $constraints)
  221. {
  222. foreach ($constraints as $constraint) {
  223. $this->addPropertyConstraint($property, $constraint);
  224. }
  225. return $this;
  226. }
  227. /**
  228. * Adds a constraint to the getter of the given property.
  229. *
  230. * The name of the getter is assumed to be the name of the property with an
  231. * uppercased first letter and either the prefix "get" or "is".
  232. *
  233. * @return $this
  234. */
  235. public function addGetterConstraint(string $property, Constraint $constraint)
  236. {
  237. if (!isset($this->getters[$property])) {
  238. $this->getters[$property] = new GetterMetadata($this->getClassName(), $property);
  239. $this->addPropertyMetadata($this->getters[$property]);
  240. }
  241. $constraint->addImplicitGroupName($this->getDefaultGroup());
  242. $this->getters[$property]->addConstraint($constraint);
  243. return $this;
  244. }
  245. /**
  246. * Adds a constraint to the getter of the given property.
  247. *
  248. * @return $this
  249. */
  250. public function addGetterMethodConstraint(string $property, string $method, Constraint $constraint)
  251. {
  252. if (!isset($this->getters[$property])) {
  253. $this->getters[$property] = new GetterMetadata($this->getClassName(), $property, $method);
  254. $this->addPropertyMetadata($this->getters[$property]);
  255. }
  256. $constraint->addImplicitGroupName($this->getDefaultGroup());
  257. $this->getters[$property]->addConstraint($constraint);
  258. return $this;
  259. }
  260. /**
  261. * @param Constraint[] $constraints
  262. *
  263. * @return $this
  264. */
  265. public function addGetterConstraints(string $property, array $constraints)
  266. {
  267. foreach ($constraints as $constraint) {
  268. $this->addGetterConstraint($property, $constraint);
  269. }
  270. return $this;
  271. }
  272. /**
  273. * @param Constraint[] $constraints
  274. *
  275. * @return $this
  276. */
  277. public function addGetterMethodConstraints(string $property, string $method, array $constraints)
  278. {
  279. foreach ($constraints as $constraint) {
  280. $this->addGetterMethodConstraint($property, $method, $constraint);
  281. }
  282. return $this;
  283. }
  284. /**
  285. * Merges the constraints of the given metadata into this object.
  286. */
  287. public function mergeConstraints(self $source)
  288. {
  289. if ($source->isGroupSequenceProvider()) {
  290. $this->setGroupSequenceProvider(true);
  291. }
  292. foreach ($source->getConstraints() as $constraint) {
  293. $this->addConstraint(clone $constraint);
  294. }
  295. foreach ($source->getConstrainedProperties() as $property) {
  296. foreach ($source->getPropertyMetadata($property) as $member) {
  297. $member = clone $member;
  298. foreach ($member->getConstraints() as $constraint) {
  299. if (\in_array($constraint::DEFAULT_GROUP, $constraint->groups, true)) {
  300. $member->constraintsByGroup[$this->getDefaultGroup()][] = $constraint;
  301. }
  302. $constraint->addImplicitGroupName($this->getDefaultGroup());
  303. }
  304. $this->addPropertyMetadata($member);
  305. if ($member instanceof MemberMetadata && !$member->isPrivate($this->name)) {
  306. $property = $member->getPropertyName();
  307. if ($member instanceof PropertyMetadata && !isset($this->properties[$property])) {
  308. $this->properties[$property] = $member;
  309. } elseif ($member instanceof GetterMetadata && !isset($this->getters[$property])) {
  310. $this->getters[$property] = $member;
  311. }
  312. }
  313. }
  314. }
  315. }
  316. /**
  317. * {@inheritdoc}
  318. */
  319. public function hasPropertyMetadata(string $property)
  320. {
  321. return \array_key_exists($property, $this->members);
  322. }
  323. /**
  324. * {@inheritdoc}
  325. */
  326. public function getPropertyMetadata(string $property)
  327. {
  328. if (!isset($this->members[$property])) {
  329. return [];
  330. }
  331. return $this->members[$property];
  332. }
  333. /**
  334. * {@inheritdoc}
  335. */
  336. public function getConstrainedProperties()
  337. {
  338. return array_keys($this->members);
  339. }
  340. /**
  341. * Sets the default group sequence for this class.
  342. *
  343. * @param string[]|GroupSequence $groupSequence An array of group names
  344. *
  345. * @return $this
  346. *
  347. * @throws GroupDefinitionException
  348. */
  349. public function setGroupSequence($groupSequence)
  350. {
  351. if ($this->isGroupSequenceProvider()) {
  352. throw new GroupDefinitionException('Defining a static group sequence is not allowed with a group sequence provider.');
  353. }
  354. if (\is_array($groupSequence)) {
  355. $groupSequence = new GroupSequence($groupSequence);
  356. }
  357. if (\in_array(Constraint::DEFAULT_GROUP, $groupSequence->groups, true)) {
  358. throw new GroupDefinitionException(sprintf('The group "%s" is not allowed in group sequences.', Constraint::DEFAULT_GROUP));
  359. }
  360. if (!\in_array($this->getDefaultGroup(), $groupSequence->groups, true)) {
  361. throw new GroupDefinitionException(sprintf('The group "%s" is missing in the group sequence.', $this->getDefaultGroup()));
  362. }
  363. $this->groupSequence = $groupSequence;
  364. return $this;
  365. }
  366. /**
  367. * {@inheritdoc}
  368. */
  369. public function hasGroupSequence()
  370. {
  371. return $this->groupSequence && \count($this->groupSequence->groups) > 0;
  372. }
  373. /**
  374. * {@inheritdoc}
  375. */
  376. public function getGroupSequence()
  377. {
  378. return $this->groupSequence;
  379. }
  380. /**
  381. * Returns a ReflectionClass instance for this class.
  382. *
  383. * @return \ReflectionClass
  384. */
  385. public function getReflectionClass()
  386. {
  387. if (!$this->reflClass) {
  388. $this->reflClass = new \ReflectionClass($this->getClassName());
  389. }
  390. return $this->reflClass;
  391. }
  392. /**
  393. * Sets whether a group sequence provider should be used.
  394. *
  395. * @throws GroupDefinitionException
  396. */
  397. public function setGroupSequenceProvider(bool $active)
  398. {
  399. if ($this->hasGroupSequence()) {
  400. throw new GroupDefinitionException('Defining a group sequence provider is not allowed with a static group sequence.');
  401. }
  402. if (!$this->getReflectionClass()->implementsInterface('Symfony\Component\Validator\GroupSequenceProviderInterface')) {
  403. throw new GroupDefinitionException(sprintf('Class "%s" must implement GroupSequenceProviderInterface.', $this->name));
  404. }
  405. $this->groupSequenceProvider = $active;
  406. }
  407. /**
  408. * {@inheritdoc}
  409. */
  410. public function isGroupSequenceProvider()
  411. {
  412. return $this->groupSequenceProvider;
  413. }
  414. /**
  415. * {@inheritdoc}
  416. */
  417. public function getCascadingStrategy()
  418. {
  419. return $this->cascadingStrategy;
  420. }
  421. private function addPropertyMetadata(PropertyMetadataInterface $metadata)
  422. {
  423. $property = $metadata->getPropertyName();
  424. $this->members[$property][] = $metadata;
  425. }
  426. private function checkConstraint(Constraint $constraint)
  427. {
  428. if (!\in_array(Constraint::CLASS_CONSTRAINT, (array) $constraint->getTargets(), true)) {
  429. throw new ConstraintDefinitionException(sprintf('The constraint "%s" cannot be put on classes.', get_debug_type($constraint)));
  430. }
  431. if ($constraint instanceof Composite) {
  432. foreach ($constraint->getNestedContraints() as $nestedContraint) {
  433. $this->checkConstraint($nestedContraint);
  434. }
  435. }
  436. }
  437. }