4b8350dc1f5ba6f80bfcd209d6c206fd5c50fd09
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Validation / ValidatorResolver.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Validation;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2009 Jochen Rau <jochen.rau@typoplanet.de>
8 * All rights reserved
9 *
10 * This class is a backport of the corresponding class of FLOW3.
11 * All credits go to the v5 team.
12 *
13 * This script is part of the TYPO3 project. The TYPO3 project is
14 * free software; you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation; either version 2 of the License, or
17 * (at your option) any later version.
18 *
19 * The GNU General Public License can be found at
20 * http://www.gnu.org/copyleft/gpl.html.
21 *
22 * This script is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
29 /**
30 * Validator resolver to automatically find a appropriate validator for a given subject
31 */
32 class ValidatorResolver implements \TYPO3\CMS\Core\SingletonInterface {
33
34 /**
35 * Match validator names and options
36 *
37 * @var string
38 */
39 const PATTERN_MATCH_VALIDATORS = '/
40 (?:^|,\\s*)
41 (?P<validatorName>[a-z0-9_:]+)
42 \\s*
43 (?:\\(
44 (?P<validatorOptions>(?:\\s*[a-z0-9]+\\s*=\\s*(?:
45 "(?:\\\\"|[^"])*"
46 |\'(?:\\\\\'|[^\'])*\'
47 |(?:\\s|[^,"\']*)
48 )(?:\\s|,)*)*)
49 \\))?
50 /ixS';
51 /**
52 * Match validator options (to parse actual options)
53 *
54 * @var string
55 */
56 const PATTERN_MATCH_VALIDATOROPTIONS = '/
57 \\s*
58 (?P<optionName>[a-z0-9]+)
59 \\s*=\\s*
60 (?P<optionValue>
61 "(?:\\\\"|[^"])*"
62 |\'(?:\\\\\'|[^\'])*\'
63 |(?:\\s|[^,"\']*)
64 )
65 /ixS';
66 /**
67 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
68 */
69 protected $objectManager;
70
71 /**
72 * @var \TYPO3\CMS\Extbase\Reflection\Service
73 */
74 protected $reflectionService;
75
76 /**
77 * @var array
78 */
79 protected $baseValidatorConjunctions = array();
80
81 /**
82 * Injects the object manager
83 *
84 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager A reference to the object manager
85 * @return void
86 */
87 public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager) {
88 $this->objectManager = $objectManager;
89 }
90
91 /**
92 * Injects the reflection service
93 *
94 * @param \TYPO3\CMS\Extbase\Reflection\Service $reflectionService
95 * @return void
96 */
97 public function injectReflectionService(\TYPO3\CMS\Extbase\Reflection\Service $reflectionService) {
98 $this->reflectionService = $reflectionService;
99 }
100
101 /**
102 * Get a validator for a given data type. Returns a validator implementing
103 * the Tx_Extbase_Validation_Validator_ValidatorInterface or NULL if no validator
104 * could be resolved.
105 *
106 * @param string $validatorName Either one of the built-in data types or fully qualified validator class name
107 * @param array $validatorOptions Options to be passed to the validator
108 * @return \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface Validator or NULL if none found.
109 */
110 public function createValidator($validatorName, array $validatorOptions = array()) {
111 $validatorClassName = $this->resolveValidatorObjectName($validatorName);
112 if ($validatorClassName === FALSE) {
113 return NULL;
114 }
115 $validator = $this->objectManager->get($validatorClassName, $validatorOptions);
116 if (!$validator instanceof \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface) {
117 return NULL;
118 }
119 if (method_exists($validator, 'setOptions')) {
120 // @deprecated since Extbase 1.4.0, will be removed in Extbase 6.1
121 $validator->setOptions($validatorOptions);
122 }
123 return $validator;
124 }
125
126 /**
127 * Resolves and returns the base validator conjunction for the given data type.
128 *
129 * If no validator could be resolved (which usually means that no validation is necessary),
130 * NULL is returned.
131 *
132 * @param string $dataType The data type to search a validator for. Usually the fully qualified object name
133 * @return \TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator The validator conjunction or NULL
134 */
135 public function getBaseValidatorConjunction($dataType) {
136 if (!isset($this->baseValidatorConjunctions[$dataType])) {
137 $this->baseValidatorConjunctions[$dataType] = $this->buildBaseValidatorConjunction($dataType);
138 }
139 return $this->baseValidatorConjunctions[$dataType];
140 }
141
142 /**
143 * Detects and registers any validators for arguments:
144 * - by the data type specified in the
145 *
146 * @param string $className
147 * @param string $methodName
148 * @throws Exception\NoSuchValidatorException
149 * @throws Exception\InvalidValidationConfigurationException
150 * @return array An Array of ValidatorConjunctions for each method parameters.
151 */
152 public function buildMethodArgumentsValidatorConjunctions($className, $methodName) {
153 $validatorConjunctions = array();
154 $methodParameters = $this->reflectionService->getMethodParameters($className, $methodName);
155 $methodTagsValues = $this->reflectionService->getMethodTagsValues($className, $methodName);
156 if (!count($methodParameters)) {
157 // early return in case no parameters were found.
158 return $validatorConjunctions;
159 }
160 foreach ($methodParameters as $parameterName => $methodParameter) {
161 $validatorConjunction = $this->createValidator('Conjunction');
162 $typeValidator = $this->createValidator($methodParameter['type']);
163 if ($typeValidator !== NULL) {
164 $validatorConjunction->addValidator($typeValidator);
165 }
166 $validatorConjunctions[$parameterName] = $validatorConjunction;
167 }
168 if (isset($methodTagsValues['validate'])) {
169 foreach ($methodTagsValues['validate'] as $validateValue) {
170 $parsedAnnotation = $this->parseValidatorAnnotation($validateValue);
171 foreach ($parsedAnnotation['validators'] as $validatorConfiguration) {
172 $newValidator = $this->createValidator($validatorConfiguration['validatorName'], $validatorConfiguration['validatorOptions']);
173 if ($newValidator === NULL) {
174 throw new \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException('Invalid validate annotation in ' . $className . '->' . $methodName . '(): Could not resolve class name for validator "' . $validatorConfiguration['validatorName'] . '".', 1239853109);
175 }
176 if (isset($validatorConjunctions[$parsedAnnotation['argumentName']])) {
177 $validatorConjunctions[$parsedAnnotation['argumentName']]->addValidator($newValidator);
178 } else {
179 throw new \TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationConfigurationException('Invalid validate annotation in ' . $className . '->' . $methodName . '(): Validator specified for argument name "' . $parsedAnnotation['argumentName'] . '", but this argument does not exist.', 1253172726);
180 }
181 }
182 }
183 }
184 return $validatorConjunctions;
185 }
186
187 /**
188 * Builds a base validator conjunction for the given data type.
189 *
190 * The base validation rules are those which were declared directly in a class (typically
191 * a model) through some @validate annotations on properties.
192 *
193 * Additionally, if a custom validator was defined for the class in question, it will be added
194 * to the end of the conjunction. A custom validator is found if it follows the naming convention
195 * "Replace '\Model\' by '\Validator\' and append "Validator".
196 *
197 * Example: $dataType is F3\Foo\Domain\Model\Quux, then the Validator will be found if it has the
198 * name F3\Foo\Domain\Validator\QuuxValidator
199 *
200 * @param string $dataType The data type to build the validation conjunction for. Needs to be the fully qualified object name.
201 * @throws Exception\NoSuchValidatorException
202 * @return \TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator The validator conjunction or NULL
203 */
204 protected function buildBaseValidatorConjunction($dataType) {
205 $validatorConjunction = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Validation\\Validator\\ConjunctionValidator');
206 // Model based validator
207 if (class_exists($dataType)) {
208 $validatorCount = 0;
209 $objectValidator = $this->createValidator('GenericObject');
210 foreach ($this->reflectionService->getClassPropertyNames($dataType) as $classPropertyName) {
211 $classPropertyTagsValues = $this->reflectionService->getPropertyTagsValues($dataType, $classPropertyName);
212 if (!isset($classPropertyTagsValues['validate'])) {
213 continue;
214 }
215 foreach ($classPropertyTagsValues['validate'] as $validateValue) {
216 $parsedAnnotation = $this->parseValidatorAnnotation($validateValue);
217 foreach ($parsedAnnotation['validators'] as $validatorConfiguration) {
218 $newValidator = $this->createValidator($validatorConfiguration['validatorName'], $validatorConfiguration['validatorOptions']);
219 if ($newValidator === NULL) {
220 throw new \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException('Invalid validate annotation in ' . $dataType . '::' . $classPropertyName . ': Could not resolve class name for validator "' . $validatorConfiguration['validatorName'] . '".', 1241098027);
221 }
222 $objectValidator->addPropertyValidator($classPropertyName, $newValidator);
223 $validatorCount++;
224 }
225 }
226 }
227 if ($validatorCount > 0) {
228 $validatorConjunction->addValidator($objectValidator);
229 }
230 }
231 // Custom validator for the class
232 $possibleValidatorClassName = str_replace('_Model_', '_Validator_', $dataType) . 'Validator';
233 $customValidator = $this->createValidator($possibleValidatorClassName);
234 if ($customValidator !== NULL) {
235 $validatorConjunction->addValidator($customValidator);
236 }
237 return $validatorConjunction;
238 }
239
240 /**
241 * Parses the validator options given in @validate annotations.
242 *
243 * @param string $validateValue
244 * @return array
245 */
246 protected function parseValidatorAnnotation($validateValue) {
247 $matches = array();
248 if ($validateValue[0] === '$') {
249 $parts = explode(' ', $validateValue, 2);
250 $validatorConfiguration = array('argumentName' => ltrim($parts[0], '$'), 'validators' => array());
251 preg_match_all(self::PATTERN_MATCH_VALIDATORS, $parts[1], $matches, PREG_SET_ORDER);
252 } else {
253 $validatorConfiguration = array('validators' => array());
254 preg_match_all(self::PATTERN_MATCH_VALIDATORS, $validateValue, $matches, PREG_SET_ORDER);
255 }
256 foreach ($matches as $match) {
257 $validatorOptions = array();
258 if (isset($match['validatorOptions'])) {
259 $validatorOptions = $this->parseValidatorOptions($match['validatorOptions']);
260 }
261 $validatorConfiguration['validators'][] = array('validatorName' => $match['validatorName'], 'validatorOptions' => $validatorOptions);
262 }
263 return $validatorConfiguration;
264 }
265
266 /**
267 * Parses $rawValidatorOptions not containing quoted option values.
268 * $rawValidatorOptions will be an empty string afterwards (pass by ref!).
269 *
270 * @param string &$rawValidatorOptions
271 * @return array An array of optionName/optionValue pairs
272 */
273 protected function parseValidatorOptions($rawValidatorOptions) {
274 $validatorOptions = array();
275 $parsedValidatorOptions = array();
276 preg_match_all(self::PATTERN_MATCH_VALIDATOROPTIONS, $rawValidatorOptions, $validatorOptions, PREG_SET_ORDER);
277 foreach ($validatorOptions as $validatorOption) {
278 $parsedValidatorOptions[trim($validatorOption['optionName'])] = trim($validatorOption['optionValue']);
279 }
280 array_walk($parsedValidatorOptions, array($this, 'unquoteString'));
281 return $parsedValidatorOptions;
282 }
283
284 /**
285 * Removes escapings from a given argument string and trims the outermost
286 * quotes.
287 *
288 * This method is meant as a helper for regular expression results.
289 *
290 * @param string &$quotedValue Value to unquote
291 */
292 protected function unquoteString(&$quotedValue) {
293 switch ($quotedValue[0]) {
294 case '"':
295 $quotedValue = str_replace('\\"', '"', trim($quotedValue, '"'));
296 break;
297 case '\'':
298 $quotedValue = str_replace('\\\'', '\'', trim($quotedValue, '\''));
299 break;
300 }
301 $quotedValue = str_replace('\\\\', '\\', $quotedValue);
302 }
303
304 /**
305 * Returns an object of an appropriate validator for the given class. If no validator is available
306 * FALSE is returned
307 *
308 * @param string $validatorName Either the fully qualified class name of the validator or the short name of a built-in validator
309 * @return string Name of the validator object or FALSE
310 */
311 protected function resolveValidatorObjectName($validatorName) {
312 if (strpos($validatorName, '_') !== FALSE && class_exists($validatorName)) {
313 return $validatorName;
314 }
315 list($extensionName, $extensionValidatorName) = explode(':', $validatorName);
316 if (!$extensionValidatorName) {
317 $possibleClassName = 'Tx_Extbase_Validation_Validator_' . $this->unifyDataType($validatorName) . 'Validator';
318 } else {
319 $possibleClassName = 'Tx_' . $extensionName . '_Validation_Validator_' . $extensionValidatorName . 'Validator';
320 }
321 if (class_exists($possibleClassName)) {
322 return $possibleClassName;
323 }
324 return FALSE;
325 }
326
327 /**
328 * Preprocess data types. Used to map primitive PHP types to DataTypes used in Extbase.
329 *
330 * @param string $type Data type to unify
331 * @return string unified data type
332 */
333 protected function unifyDataType($type) {
334 switch ($type) {
335 case 'int':
336 $type = 'Integer';
337 break;
338 case 'bool':
339 $type = 'Boolean';
340 break;
341 case 'double':
342 $type = 'Float';
343 break;
344 case 'numeric':
345 $type = 'Number';
346 break;
347 case 'mixed':
348 $type = 'Raw';
349 break;
350 }
351 return ucfirst($type);
352 }
353
354 }
355
356
357 ?>