RecursiveContextualValidator.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. <?php
  2. namespace Drupal\Core\TypedData\Validation;
  3. use Drupal\Core\TypedData\ComplexDataInterface;
  4. use Drupal\Core\TypedData\ListInterface;
  5. use Drupal\Core\TypedData\TypedDataInterface;
  6. use Drupal\Core\TypedData\TypedDataManagerInterface;
  7. use Symfony\Component\Validator\Constraint;
  8. use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
  9. use Symfony\Component\Validator\Context\ExecutionContextInterface;
  10. use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
  11. use Symfony\Component\Validator\Util\PropertyPath;
  12. /**
  13. * Defines a recursive contextual validator for Typed Data.
  14. *
  15. * For both list and complex data it call recursively out to the properties /
  16. * elements of the list.
  17. *
  18. * This class calls out to some methods on the execution context marked as
  19. * internal. These methods are internal to the validator (which is implemented
  20. * by this class) but should not be called by users.
  21. * See http://symfony.com/doc/current/contributing/code/bc.html for more
  22. * information about @internal.
  23. *
  24. * @see \Drupal\Core\TypedData\Validation\RecursiveValidator::startContext()
  25. * @see \Drupal\Core\TypedData\Validation\RecursiveValidator::inContext()
  26. */
  27. class RecursiveContextualValidator implements ContextualValidatorInterface {
  28. /**
  29. * The execution context.
  30. *
  31. * @var \Symfony\Component\Validator\Context\ExecutionContextInterface
  32. */
  33. protected $context;
  34. /**
  35. * The metadata factory.
  36. *
  37. * @var \Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface
  38. */
  39. protected $metadataFactory;
  40. /**
  41. * The constraint validator factory.
  42. *
  43. * @var \Symfony\Component\Validator\ConstraintValidatorFactoryInterface
  44. */
  45. protected $constraintValidatorFactory;
  46. /**
  47. * Creates a validator for the given context.
  48. *
  49. * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
  50. * The factory for creating new contexts.
  51. * @param \Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface $metadata_factory
  52. * The metadata factory.
  53. * @param \Symfony\Component\Validator\ConstraintValidatorFactoryInterface $validator_factory
  54. * The constraint validator factory.
  55. * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
  56. * The typed data manager.
  57. */
  58. public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadata_factory, ConstraintValidatorFactoryInterface $validator_factory, TypedDataManagerInterface $typed_data_manager) {
  59. $this->context = $context;
  60. $this->metadataFactory = $metadata_factory;
  61. $this->constraintValidatorFactory = $validator_factory;
  62. $this->typedDataManager = $typed_data_manager;
  63. }
  64. /**
  65. * {@inheritdoc}
  66. */
  67. public function atPath($path) {
  68. // @todo This method is not used at the moment, see
  69. // https://www.drupal.org/node/2482527
  70. return $this;
  71. }
  72. /**
  73. * {@inheritdoc}
  74. */
  75. public function validate($data, $constraints = NULL, $groups = NULL, $is_root_call = TRUE) {
  76. if (isset($groups)) {
  77. throw new \LogicException('Passing custom groups is not supported.');
  78. }
  79. if (!$data instanceof TypedDataInterface) {
  80. throw new \InvalidArgumentException('The passed value must be a typed data object.');
  81. }
  82. // You can pass a single constraint or an array of constraints.
  83. // Make sure to deal with an array in the rest of the code.
  84. if (isset($constraints) && !is_array($constraints)) {
  85. $constraints = [$constraints];
  86. }
  87. $this->validateNode($data, $constraints, $is_root_call);
  88. return $this;
  89. }
  90. /**
  91. * Validates a Typed Data node in the validation tree.
  92. *
  93. * If no constraints are passed, the data is validated against the
  94. * constraints specified in its data definition. If the data is complex or a
  95. * list and no constraints are passed, the contained properties or list items
  96. * are validated recursively.
  97. *
  98. * @param \Drupal\Core\TypedData\TypedDataInterface $data
  99. * The data to validated.
  100. * @param \Symfony\Component\Validator\Constraint[]|null $constraints
  101. * (optional) If set, an array of constraints to validate.
  102. * @param bool $is_root_call
  103. * (optional) Whether its the most upper call in the type data tree.
  104. *
  105. * @return $this
  106. */
  107. protected function validateNode(TypedDataInterface $data, $constraints = NULL, $is_root_call = FALSE) {
  108. $previous_value = $this->context->getValue();
  109. $previous_object = $this->context->getObject();
  110. $previous_metadata = $this->context->getMetadata();
  111. $previous_path = $this->context->getPropertyPath();
  112. $metadata = $this->metadataFactory->getMetadataFor($data);
  113. $cache_key = spl_object_hash($data);
  114. $property_path = $is_root_call ? '' : PropertyPath::append($previous_path, $data->getName());
  115. // Pass the canonical representation of the data as validated value to
  116. // constraint validators, such that they do not have to care about Typed
  117. // Data.
  118. $value = $this->typedDataManager->getCanonicalRepresentation($data);
  119. $this->context->setNode($value, $data, $metadata, $property_path);
  120. if (isset($constraints) || !$this->context->isGroupValidated($cache_key, Constraint::DEFAULT_GROUP)) {
  121. if (!isset($constraints)) {
  122. $this->context->markGroupAsValidated($cache_key, Constraint::DEFAULT_GROUP);
  123. $constraints = $metadata->findConstraints(Constraint::DEFAULT_GROUP);
  124. }
  125. $this->validateConstraints($value, $cache_key, $constraints);
  126. }
  127. // If the data is a list or complex data, validate the contained list items
  128. // or properties. However, do not recurse if the data is empty.
  129. if (($data instanceof ListInterface || $data instanceof ComplexDataInterface) && !$data->isEmpty()) {
  130. foreach ($data as $name => $property) {
  131. $this->validateNode($property);
  132. }
  133. }
  134. $this->context->setNode($previous_value, $previous_object, $previous_metadata, $previous_path);
  135. return $this;
  136. }
  137. /**
  138. * Validates a node's value against all constraints in the given group.
  139. *
  140. * @param mixed $value
  141. * The validated value.
  142. * @param string $cache_key
  143. * The cache key used internally to ensure we don't validate the same
  144. * constraint twice.
  145. * @param \Symfony\Component\Validator\Constraint[] $constraints
  146. * The constraints which should be ensured for the given value.
  147. */
  148. protected function validateConstraints($value, $cache_key, $constraints) {
  149. foreach ($constraints as $constraint) {
  150. // Prevent duplicate validation of constraints, in the case
  151. // that constraints belong to multiple validated groups
  152. if (isset($cache_key)) {
  153. $constraint_hash = spl_object_hash($constraint);
  154. if ($this->context->isConstraintValidated($cache_key, $constraint_hash)) {
  155. continue;
  156. }
  157. $this->context->markConstraintAsValidated($cache_key, $constraint_hash);
  158. }
  159. $this->context->setConstraint($constraint);
  160. $validator = $this->constraintValidatorFactory->getInstance($constraint);
  161. $validator->initialize($this->context);
  162. $validator->validate($value, $constraint);
  163. }
  164. }
  165. /**
  166. * {@inheritdoc}
  167. */
  168. public function getViolations() {
  169. return $this->context->getViolations();
  170. }
  171. /**
  172. * {@inheritdoc}
  173. */
  174. public function validateProperty($object, $propertyName, $groups = NULL) {
  175. if (isset($groups)) {
  176. throw new \LogicException('Passing custom groups is not supported.');
  177. }
  178. if (!is_object($object)) {
  179. throw new \InvalidArgumentException('Passing class name is not supported.');
  180. }
  181. elseif (!$object instanceof TypedDataInterface) {
  182. throw new \InvalidArgumentException('The passed in object has to be typed data.');
  183. }
  184. elseif (!$object instanceof ListInterface && !$object instanceof ComplexDataInterface) {
  185. throw new \InvalidArgumentException('Passed data does not contain properties.');
  186. }
  187. return $this->validateNode($object->get($propertyName), NULL, TRUE);
  188. }
  189. /**
  190. * {@inheritdoc}
  191. */
  192. public function validatePropertyValue($object, $property_name, $value, $groups = NULL) {
  193. if (!is_object($object)) {
  194. throw new \InvalidArgumentException('Passing class name is not supported.');
  195. }
  196. elseif (!$object instanceof TypedDataInterface) {
  197. throw new \InvalidArgumentException('The passed in object has to be typed data.');
  198. }
  199. elseif (!$object instanceof ListInterface && !$object instanceof ComplexDataInterface) {
  200. throw new \InvalidArgumentException('Passed data does not contain properties.');
  201. }
  202. $data = $object->get($property_name);
  203. $metadata = $this->metadataFactory->getMetadataFor($data);
  204. $constraints = $metadata->findConstraints(Constraint::DEFAULT_GROUP);
  205. return $this->validate($value, $constraints, $groups, TRUE);
  206. }
  207. }