3 namespace Drupal\Core\TypedData\Validation;
5 use Drupal\Core\TypedData\ComplexDataInterface;
6 use Drupal\Core\TypedData\ListInterface;
7 use Drupal\Core\TypedData\TypedDataInterface;
8 use Drupal\Core\TypedData\TypedDataManagerInterface;
9 use Symfony\Component\Validator\Constraint;
10 use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
11 use Symfony\Component\Validator\Context\ExecutionContextInterface;
12 use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
13 use Symfony\Component\Validator\Util\PropertyPath;
16 * Defines a recursive contextual validator for Typed Data.
18 * For both list and complex data it call recursively out to the properties /
19 * elements of the list.
21 * This class calls out to some methods on the execution context marked as
22 * internal. These methods are internal to the validator (which is implemented
23 * by this class) but should not be called by users.
24 * See http://symfony.com/doc/current/contributing/code/bc.html for more
25 * information about @internal.
27 * @see \Drupal\Core\TypedData\Validation\RecursiveValidator::startContext()
28 * @see \Drupal\Core\TypedData\Validation\RecursiveValidator::inContext()
30 class RecursiveContextualValidator implements ContextualValidatorInterface {
33 * The execution context.
35 * @var \Symfony\Component\Validator\Context\ExecutionContextInterface
40 * The metadata factory.
42 * @var \Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface
44 protected $metadataFactory;
47 * The constraint validator factory.
49 * @var \Symfony\Component\Validator\ConstraintValidatorFactoryInterface
51 protected $constraintValidatorFactory;
54 * Creates a validator for the given context.
56 * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
57 * The factory for creating new contexts.
58 * @param \Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface $metadata_factory
59 * The metadata factory.
60 * @param \Symfony\Component\Validator\ConstraintValidatorFactoryInterface $validator_factory
61 * The constraint validator factory.
62 * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
63 * The typed data manager.
65 public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadata_factory, ConstraintValidatorFactoryInterface $validator_factory, TypedDataManagerInterface $typed_data_manager) {
66 $this->context = $context;
67 $this->metadataFactory = $metadata_factory;
68 $this->constraintValidatorFactory = $validator_factory;
69 $this->typedDataManager = $typed_data_manager;
75 public function atPath($path) {
76 // @todo This method is not used at the moment, see
77 // https://www.drupal.org/node/2482527
84 public function validate($data, $constraints = NULL, $groups = NULL, $is_root_call = TRUE) {
86 throw new \LogicException('Passing custom groups is not supported.');
89 if (!$data instanceof TypedDataInterface) {
90 throw new \InvalidArgumentException('The passed value must be a typed data object.');
93 // You can pass a single constraint or an array of constraints.
94 // Make sure to deal with an array in the rest of the code.
95 if (isset($constraints) && !is_array($constraints)) {
96 $constraints = [$constraints];
99 $this->validateNode($data, $constraints, $is_root_call);
104 * Validates a Typed Data node in the validation tree.
106 * If no constraints are passed, the data is validated against the
107 * constraints specified in its data definition. If the data is complex or a
108 * list and no constraints are passed, the contained properties or list items
109 * are validated recursively.
111 * @param \Drupal\Core\TypedData\TypedDataInterface $data
112 * The data to validated.
113 * @param \Symfony\Component\Validator\Constraint[]|null $constraints
114 * (optional) If set, an array of constraints to validate.
115 * @param bool $is_root_call
116 * (optional) Whether its the most upper call in the type data tree.
120 protected function validateNode(TypedDataInterface $data, $constraints = NULL, $is_root_call = FALSE) {
121 $previous_value = $this->context->getValue();
122 $previous_object = $this->context->getObject();
123 $previous_metadata = $this->context->getMetadata();
124 $previous_path = $this->context->getPropertyPath();
126 $metadata = $this->metadataFactory->getMetadataFor($data);
127 $cache_key = spl_object_hash($data);
128 $property_path = $is_root_call ? '' : PropertyPath::append($previous_path, $data->getName());
130 // Prefer a specific instance of the typed data manager stored by the data
131 // if it is available. This is necessary for specialized typed data objects,
132 // for example those using the typed config subclass of the manager.
133 $typed_data_manager = method_exists($data, 'getTypedDataManager') ? $data->getTypedDataManager() : $this->typedDataManager;
135 // Pass the canonical representation of the data as validated value to
136 // constraint validators, such that they do not have to care about Typed
138 $value = $typed_data_manager->getCanonicalRepresentation($data);
139 $this->context->setNode($value, $data, $metadata, $property_path);
141 if (isset($constraints) || !$this->context->isGroupValidated($cache_key, Constraint::DEFAULT_GROUP)) {
142 if (!isset($constraints)) {
143 $this->context->markGroupAsValidated($cache_key, Constraint::DEFAULT_GROUP);
144 $constraints = $metadata->findConstraints(Constraint::DEFAULT_GROUP);
146 $this->validateConstraints($value, $cache_key, $constraints);
149 // If the data is a list or complex data, validate the contained list items
150 // or properties. However, do not recurse if the data is empty.
151 if (($data instanceof ListInterface || $data instanceof ComplexDataInterface) && !$data->isEmpty()) {
152 foreach ($data as $name => $property) {
153 $this->validateNode($property);
157 $this->context->setNode($previous_value, $previous_object, $previous_metadata, $previous_path);
163 * Validates a node's value against all constraints in the given group.
165 * @param mixed $value
166 * The validated value.
167 * @param string $cache_key
168 * The cache key used internally to ensure we don't validate the same
170 * @param \Symfony\Component\Validator\Constraint[] $constraints
171 * The constraints which should be ensured for the given value.
173 protected function validateConstraints($value, $cache_key, $constraints) {
174 foreach ($constraints as $constraint) {
175 // Prevent duplicate validation of constraints, in the case
176 // that constraints belong to multiple validated groups
177 if (isset($cache_key)) {
178 $constraint_hash = spl_object_hash($constraint);
180 if ($this->context->isConstraintValidated($cache_key, $constraint_hash)) {
184 $this->context->markConstraintAsValidated($cache_key, $constraint_hash);
187 $this->context->setConstraint($constraint);
189 $validator = $this->constraintValidatorFactory->getInstance($constraint);
190 $validator->initialize($this->context);
191 $validator->validate($value, $constraint);
198 public function getViolations() {
199 return $this->context->getViolations();
205 public function validateProperty($object, $propertyName, $groups = NULL) {
206 if (isset($groups)) {
207 throw new \LogicException('Passing custom groups is not supported.');
209 if (!is_object($object)) {
210 throw new \InvalidArgumentException('Passing class name is not supported.');
212 elseif (!$object instanceof TypedDataInterface) {
213 throw new \InvalidArgumentException('The passed in object has to be typed data.');
215 elseif (!$object instanceof ListInterface && !$object instanceof ComplexDataInterface) {
216 throw new \InvalidArgumentException('Passed data does not contain properties.');
218 return $this->validateNode($object->get($propertyName), NULL, TRUE);
224 public function validatePropertyValue($object, $property_name, $value, $groups = NULL) {
225 if (!is_object($object)) {
226 throw new \InvalidArgumentException('Passing class name is not supported.');
228 elseif (!$object instanceof TypedDataInterface) {
229 throw new \InvalidArgumentException('The passed in object has to be typed data.');
231 elseif (!$object instanceof ListInterface && !$object instanceof ComplexDataInterface) {
232 throw new \InvalidArgumentException('Passed data does not contain properties.');
234 $data = $object->get($property_name);
235 $metadata = $this->metadataFactory->getMetadataFor($data);
236 $constraints = $metadata->findConstraints(Constraint::DEFAULT_GROUP);
237 return $this->validate($value, $constraints, $groups, TRUE);