Pull merge.
[yaffs-website] / web / core / lib / Drupal / Core / TypedData / Validation / RecursiveContextualValidator.php
1 <?php
2
3 namespace Drupal\Core\TypedData\Validation;
4
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;
14
15 /**
16  * Defines a recursive contextual validator for Typed Data.
17  *
18  * For both list and complex data it call recursively out to the properties /
19  * elements of the list.
20  *
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.
26  *
27  * @see \Drupal\Core\TypedData\Validation\RecursiveValidator::startContext()
28  * @see \Drupal\Core\TypedData\Validation\RecursiveValidator::inContext()
29  */
30 class RecursiveContextualValidator implements ContextualValidatorInterface {
31
32   /**
33    * The execution context.
34    *
35    * @var \Symfony\Component\Validator\Context\ExecutionContextInterface
36    */
37   protected $context;
38
39   /**
40    * The metadata factory.
41    *
42    * @var \Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface
43    */
44   protected $metadataFactory;
45
46   /**
47    * The constraint validator factory.
48    *
49    * @var \Symfony\Component\Validator\ConstraintValidatorFactoryInterface
50    */
51   protected $constraintValidatorFactory;
52
53   /**
54    * Creates a validator for the given context.
55    *
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.
64    */
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;
70   }
71
72   /**
73    * {@inheritdoc}
74    */
75   public function atPath($path) {
76     // @todo This method is not used at the moment, see
77     //   https://www.drupal.org/node/2482527
78     return $this;
79   }
80
81   /**
82    * {@inheritdoc}
83    */
84   public function validate($data, $constraints = NULL, $groups = NULL, $is_root_call = TRUE) {
85     if (isset($groups)) {
86       throw new \LogicException('Passing custom groups is not supported.');
87     }
88
89     if (!$data instanceof TypedDataInterface) {
90       throw new \InvalidArgumentException('The passed value must be a typed data object.');
91     }
92
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];
97     }
98
99     $this->validateNode($data, $constraints, $is_root_call);
100     return $this;
101   }
102
103   /**
104    * Validates a Typed Data node in the validation tree.
105    *
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.
110    *
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.
117    *
118    * @return $this
119    */
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();
125
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());
129
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;
134
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
137     // Data.
138     $value = $typed_data_manager->getCanonicalRepresentation($data);
139     $this->context->setNode($value, $data, $metadata, $property_path);
140
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);
145       }
146       $this->validateConstraints($value, $cache_key, $constraints);
147     }
148
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);
154       }
155     }
156
157     $this->context->setNode($previous_value, $previous_object, $previous_metadata, $previous_path);
158
159     return $this;
160   }
161
162   /**
163    * Validates a node's value against all constraints in the given group.
164    *
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
169    *   constraint twice.
170    * @param \Symfony\Component\Validator\Constraint[] $constraints
171    *   The constraints which should be ensured for the given value.
172    */
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);
179
180         if ($this->context->isConstraintValidated($cache_key, $constraint_hash)) {
181           continue;
182         }
183
184         $this->context->markConstraintAsValidated($cache_key, $constraint_hash);
185       }
186
187       $this->context->setConstraint($constraint);
188
189       $validator = $this->constraintValidatorFactory->getInstance($constraint);
190       $validator->initialize($this->context);
191       $validator->validate($value, $constraint);
192     }
193   }
194
195   /**
196    * {@inheritdoc}
197    */
198   public function getViolations() {
199     return $this->context->getViolations();
200   }
201
202   /**
203    * {@inheritdoc}
204    */
205   public function validateProperty($object, $propertyName, $groups = NULL) {
206     if (isset($groups)) {
207       throw new \LogicException('Passing custom groups is not supported.');
208     }
209     if (!is_object($object)) {
210       throw new \InvalidArgumentException('Passing class name is not supported.');
211     }
212     elseif (!$object instanceof TypedDataInterface) {
213       throw new \InvalidArgumentException('The passed in object has to be typed data.');
214     }
215     elseif (!$object instanceof ListInterface && !$object instanceof ComplexDataInterface) {
216       throw new \InvalidArgumentException('Passed data does not contain properties.');
217     }
218     return $this->validateNode($object->get($propertyName), NULL, TRUE);
219   }
220
221   /**
222    * {@inheritdoc}
223    */
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.');
227     }
228     elseif (!$object instanceof TypedDataInterface) {
229       throw new \InvalidArgumentException('The passed in object has to be typed data.');
230     }
231     elseif (!$object instanceof ListInterface && !$object instanceof ComplexDataInterface) {
232       throw new \InvalidArgumentException('Passed data does not contain properties.');
233     }
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);
238   }
239
240 }