TYPO3CMS  8
 All Classes Namespaces Files Functions Variables Pages
ValidatorResolver.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Extbase\Validation;
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
22 
27 {
35  (?:^|,\s*)
36  (?P<validatorName>[a-z0-9_:.\\\\]+)
37  \s*
38  (?:\(
39  (?P<validatorOptions>(?:\s*[a-z0-9]+\s*=\s*(?:
40  "(?:\\\\"|[^"])*"
41  |\'(?:\\\\\'|[^\'])*\'
42  |(?:\s|[^,"\']*)
43  )(?:\s|,)*)*)
44  \))?
45  /ixS';
46 
52  \s*
53  (?P<optionName>[a-z0-9]+)
54  \s*=\s*
55  (?P<optionValue>
56  "(?:\\\\"|[^"])*"
57  |\'(?:\\\\\'|[^\'])*\'
58  |(?:\s|[^,"\']*)
59  )
60  /ixS';
61 
65  protected $objectManager;
66 
70  protected $reflectionService;
71 
76 
80  public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
81  {
82  $this->objectManager = $objectManager;
83  }
84 
88  public function injectReflectionService(\TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService)
89  {
90  $this->reflectionService = $reflectionService;
91  }
92 
102  public function createValidator($validatorType, array $validatorOptions = [])
103  {
104  try {
108  $validatorObjectName = $this->resolveValidatorObjectName($validatorType);
109 
110  $validator = $this->objectManager->get($validatorObjectName, $validatorOptions);
111 
112  if (!($validator instanceof \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface)) {
113  throw new Exception\NoSuchValidatorException('The validator "' . $validatorObjectName . '" does not implement TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface!', 1300694875);
114  }
115 
116  return $validator;
117  } catch (NoSuchValidatorException $e) {
118  GeneralUtility::devLog($e->getMessage(), 'extbase', GeneralUtility::SYSLOG_SEVERITY_INFO);
119  return null;
120  }
121  }
122 
132  public function getBaseValidatorConjunction($targetClassName)
133  {
134  if (!array_key_exists($targetClassName, $this->baseValidatorConjunctions)) {
135  $this->buildBaseValidatorConjunction($targetClassName, $targetClassName);
136  }
137 
138  return $this->baseValidatorConjunctions[$targetClassName];
139  }
140 
155  public function buildMethodArgumentsValidatorConjunctions($className, $methodName, array $methodParameters = null, array $methodValidateAnnotations = null)
156  {
158  $validatorConjunctions = [];
159 
160  if ($methodParameters === null) {
161  $methodParameters = $this->reflectionService->getMethodParameters($className, $methodName);
162  }
163  if (empty($methodParameters)) {
164  return $validatorConjunctions;
165  }
166 
167  foreach ($methodParameters as $parameterName => $methodParameter) {
169  $validatorConjunction = $this->createValidator(ConjunctionValidator::class);
170 
171  if (!array_key_exists('type', $methodParameter)) {
172  throw new Exception\InvalidTypeHintException('Missing type information, probably no @param annotation for parameter "$' . $parameterName . '" in ' . $className . '->' . $methodName . '()', 1281962564);
173  }
174 
175  // @todo: remove check for old underscore model name syntax once it's possible
176  if (strpbrk($methodParameter['type'], '_\\') === false) {
177  $typeValidator = $this->createValidator($methodParameter['type']);
178  } else {
179  $typeValidator = null;
180  }
181 
182  if ($typeValidator !== null) {
183  $validatorConjunction->addValidator($typeValidator);
184  }
185  $validatorConjunctions[$parameterName] = $validatorConjunction;
186  }
187 
188  if ($methodValidateAnnotations === null) {
189  $validateAnnotations = $this->getMethodValidateAnnotations($className, $methodName);
190  $methodValidateAnnotations = array_map(function ($validateAnnotation) {
191  return [
192  'type' => $validateAnnotation['validatorName'],
193  'options' => $validateAnnotation['validatorOptions'],
194  'argumentName' => $validateAnnotation['argumentName'],
195  ];
196  }, $validateAnnotations);
197  }
198 
199  foreach ($methodValidateAnnotations as $annotationParameters) {
200  $newValidator = $this->createValidator($annotationParameters['type'], $annotationParameters['options']);
201  if ($newValidator === null) {
202  throw new Exception\NoSuchValidatorException('Invalid validate annotation in ' . $className . '->' . $methodName . '(): Could not resolve class name for validator "' . $annotationParameters['type'] . '".', 1239853109);
203  }
204  if (isset($validatorConjunctions[$annotationParameters['argumentName']])) {
205  $validatorConjunctions[$annotationParameters['argumentName']]->addValidator($newValidator);
206  } elseif (strpos($annotationParameters['argumentName'], '.') !== false) {
207  $objectPath = explode('.', $annotationParameters['argumentName']);
208  $argumentName = array_shift($objectPath);
209  $validatorConjunctions[$argumentName]->addValidator($this->buildSubObjectValidator($objectPath, $newValidator));
210  } else {
211  throw new Exception\InvalidValidationConfigurationException('Invalid validate annotation in ' . $className . '->' . $methodName . '(): Validator specified for argument name "' . $annotationParameters['argumentName'] . '", but this argument does not exist.', 1253172726);
212  }
213  }
214 
215  return $validatorConjunctions;
216  }
217 
226  protected function buildSubObjectValidator(array $objectPath, \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface $propertyValidator)
227  {
228  $rootObjectValidator = $this->objectManager->get(\TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator::class, []);
229  $parentObjectValidator = $rootObjectValidator;
230 
231  while (count($objectPath) > 1) {
232  $subObjectValidator = $this->objectManager->get(\TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator::class, []);
233  $subPropertyName = array_shift($objectPath);
234  $parentObjectValidator->addPropertyValidator($subPropertyName, $subObjectValidator);
235  $parentObjectValidator = $subObjectValidator;
236  }
237 
238  $parentObjectValidator->addPropertyValidator(array_shift($objectPath), $propertyValidator);
239 
240  return $rootObjectValidator;
241  }
242 
266  protected function buildBaseValidatorConjunction($indexKey, $targetClassName, array $validationGroups = [])
267  {
268  $conjunctionValidator = new ConjunctionValidator();
269  $this->baseValidatorConjunctions[$indexKey] = $conjunctionValidator;
270 
271  // note: the simpleType check reduces lookups to the class loader
272  if (!TypeHandlingUtility::isSimpleType($targetClassName) && class_exists($targetClassName)) {
273  // Model based validator
275  $objectValidator = $this->objectManager->get(\TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator::class, []);
276  foreach ($this->reflectionService->getClassPropertyNames($targetClassName) as $classPropertyName) {
277  $classPropertyTagsValues = $this->reflectionService->getPropertyTagsValues($targetClassName, $classPropertyName);
278 
279  if (!isset($classPropertyTagsValues['var'])) {
280  throw new \InvalidArgumentException(sprintf('There is no @var annotation for property "%s" in class "%s".', $classPropertyName, $targetClassName), 1363778104);
281  }
282  try {
283  $parsedType = TypeHandlingUtility::parseType(trim(implode('', $classPropertyTagsValues['var']), ' \\'));
284  } catch (\TYPO3\CMS\Extbase\Utility\Exception\InvalidTypeException $exception) {
285  throw new \InvalidArgumentException(sprintf(' @var annotation of ' . $exception->getMessage(), 'class "' . $targetClassName . '", property "' . $classPropertyName . '"'), 1315564744, $exception);
286  }
287  $propertyTargetClassName = $parsedType['type'];
288  // note: the outer simpleType check reduces lookups to the class loader
289  if (!TypeHandlingUtility::isSimpleType($propertyTargetClassName)) {
290  if (TypeHandlingUtility::isCollectionType($propertyTargetClassName)) {
291  $collectionValidator = $this->createValidator(\TYPO3\CMS\Extbase\Validation\Validator\CollectionValidator::class, ['elementType' => $parsedType['elementType'], 'validationGroups' => $validationGroups]);
292  $objectValidator->addPropertyValidator($classPropertyName, $collectionValidator);
293  } elseif (class_exists($propertyTargetClassName) && !TypeHandlingUtility::isCoreType($propertyTargetClassName) && $this->objectManager->isRegistered($propertyTargetClassName) && $this->objectManager->getScope($propertyTargetClassName) === \TYPO3\CMS\Extbase\Object\Container\Container::SCOPE_PROTOTYPE) {
294  $validatorForProperty = $this->getBaseValidatorConjunction($propertyTargetClassName);
295  if ($validatorForProperty !== null && $validatorForProperty->count() > 0) {
296  $objectValidator->addPropertyValidator($classPropertyName, $validatorForProperty);
297  }
298  }
299  }
300 
301  $validateAnnotations = [];
302  // @todo: Resolve annotations via reflectionService once its available
303  if (isset($classPropertyTagsValues['validate']) && is_array($classPropertyTagsValues['validate'])) {
304  foreach ($classPropertyTagsValues['validate'] as $validateValue) {
305  $parsedAnnotations = $this->parseValidatorAnnotation($validateValue);
306 
307  foreach ($parsedAnnotations['validators'] as $validator) {
308  array_push($validateAnnotations, [
309  'argumentName' => $parsedAnnotations['argumentName'],
310  'validatorName' => $validator['validatorName'],
311  'validatorOptions' => $validator['validatorOptions']
312  ]);
313  }
314  }
315  }
316 
317  foreach ($validateAnnotations as $validateAnnotation) {
318  // @todo: Respect validationGroups
319  $newValidator = $this->createValidator($validateAnnotation['validatorName'], $validateAnnotation['validatorOptions']);
320  if ($newValidator === null) {
321  throw new Exception\NoSuchValidatorException('Invalid validate annotation in ' . $targetClassName . '::' . $classPropertyName . ': Could not resolve class name for validator "' . $validateAnnotation->type . '".', 1241098027);
322  }
323  $objectValidator->addPropertyValidator($classPropertyName, $newValidator);
324  }
325  }
326 
327  if (!empty($objectValidator->getPropertyValidators())) {
328  $conjunctionValidator->addValidator($objectValidator);
329  }
330  }
331 
332  $this->addCustomValidators($targetClassName, $conjunctionValidator);
333  }
334 
349  protected function addCustomValidators($targetClassName, ConjunctionValidator &$conjunctionValidator)
350  {
351  // @todo: get rid of ClassNamingUtility usage once we dropped underscored class name support
352  $possibleValidatorClassName = ClassNamingUtility::translateModelNameToValidatorName($targetClassName);
353 
354  $customValidator = $this->createValidator($possibleValidatorClassName);
355  if ($customValidator !== null) {
356  $conjunctionValidator->addValidator($customValidator);
357  }
358 
359  // @todo: find polytype validator for class
360  }
361 
368  protected function parseValidatorAnnotation($validateValue)
369  {
370  $matches = [];
371  if ($validateValue[0] === '$') {
372  $parts = explode(' ', $validateValue, 2);
373  $validatorConfiguration = ['argumentName' => ltrim($parts[0], '$'), 'validators' => []];
374  preg_match_all(self::PATTERN_MATCH_VALIDATORS, $parts[1], $matches, PREG_SET_ORDER);
375  } else {
376  $validatorConfiguration = ['validators' => []];
377  preg_match_all(self::PATTERN_MATCH_VALIDATORS, $validateValue, $matches, PREG_SET_ORDER);
378  }
379  foreach ($matches as $match) {
380  $validatorOptions = [];
381  if (isset($match['validatorOptions'])) {
382  $validatorOptions = $this->parseValidatorOptions($match['validatorOptions']);
383  }
384  $validatorConfiguration['validators'][] = ['validatorName' => $match['validatorName'], 'validatorOptions' => $validatorOptions];
385  }
386  return $validatorConfiguration;
387  }
388 
396  protected function parseValidatorOptions($rawValidatorOptions)
397  {
398  $validatorOptions = [];
399  $parsedValidatorOptions = [];
400  preg_match_all(self::PATTERN_MATCH_VALIDATOROPTIONS, $rawValidatorOptions, $validatorOptions, PREG_SET_ORDER);
401  foreach ($validatorOptions as $validatorOption) {
402  $parsedValidatorOptions[trim($validatorOption['optionName'])] = trim($validatorOption['optionValue']);
403  }
404  array_walk($parsedValidatorOptions, [$this, 'unquoteString']);
405  return $parsedValidatorOptions;
406  }
407 
417  protected function unquoteString(&$quotedValue)
418  {
419  switch ($quotedValue[0]) {
420  case '"':
421  $quotedValue = str_replace('\\"', '"', trim($quotedValue, '"'));
422  break;
423  case '\'':
424  $quotedValue = str_replace('\\\'', '\'', trim($quotedValue, '\''));
425  break;
426  }
427  $quotedValue = str_replace('\\\\', '\\', $quotedValue);
428  }
429 
439  protected function resolveValidatorObjectName($validatorName)
440  {
441  if (strpos($validatorName, ':') !== false || strpbrk($validatorName, '_\\') === false) {
442  // Found shorthand validator, either extbase or foreign extension
443  // NotEmpty or Acme.MyPck.Ext:MyValidator
444  list($extensionName, $extensionValidatorName) = explode(':', $validatorName);
445 
446  if ($validatorName !== $extensionName && $extensionValidatorName !== '') {
447  // Shorthand custom
448  if (strpos($extensionName, '.') !== false) {
449  $extensionNameParts = explode('.', $extensionName);
450  $extensionName = array_pop($extensionNameParts);
451  $vendorName = implode('\\', $extensionNameParts);
452  $possibleClassName = $vendorName . '\\' . $extensionName . '\\Validation\\Validator\\' . $extensionValidatorName;
453  } else {
454  $possibleClassName = 'Tx_' . $extensionName . '_Validation_Validator_' . $extensionValidatorName;
455  }
456  } else {
457  // Shorthand built in
458  $possibleClassName = 'TYPO3\\CMS\\Extbase\\Validation\\Validator\\' . $this->getValidatorType($validatorName);
459  }
460  } else {
461  // Full qualified
462  // Tx_MyExt_Validation_Validator_MyValidator or \Acme\Ext\Validation\Validator\FooValidator
463  $possibleClassName = $validatorName;
464  if (!empty($possibleClassName) && $possibleClassName[0] === '\\') {
465  $possibleClassName = substr($possibleClassName, 1);
466  }
467  }
468 
469  if (substr($possibleClassName, - strlen('Validator')) !== 'Validator') {
470  $possibleClassName .= 'Validator';
471  }
472 
473  if (class_exists($possibleClassName)) {
474  $possibleClassNameInterfaces = class_implements($possibleClassName);
475  if (!in_array(\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface::class, $possibleClassNameInterfaces)) {
476  // The guessed validatorname is a valid class name, but does not implement the ValidatorInterface
477  throw new NoSuchValidatorException('Validator class ' . $validatorName . ' must implement \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface', 1365776838);
478  }
479  $resolvedValidatorName = $possibleClassName;
480  } else {
481  throw new NoSuchValidatorException('Validator class ' . $validatorName . ' does not exist', 1365799920);
482  }
483 
484  return $resolvedValidatorName;
485  }
486 
493  protected function getValidatorType($type)
494  {
495  switch ($type) {
496  case 'int':
497  $type = 'Integer';
498  break;
499  case 'bool':
500  $type = 'Boolean';
501  break;
502  case 'double':
503  $type = 'Float';
504  break;
505  case 'numeric':
506  $type = 'Number';
507  break;
508  case 'mixed':
509  $type = 'Raw';
510  break;
511  default:
512  $type = ucfirst($type);
513  }
514  return $type;
515  }
516 
525  public function getMethodValidateAnnotations($className, $methodName)
526  {
527  $validateAnnotations = [];
528  $methodTagsValues = $this->reflectionService->getMethodTagsValues($className, $methodName);
529  if (isset($methodTagsValues['validate']) && is_array($methodTagsValues['validate'])) {
530  foreach ($methodTagsValues['validate'] as $validateValue) {
531  $parsedAnnotations = $this->parseValidatorAnnotation($validateValue);
532 
533  foreach ($parsedAnnotations['validators'] as $validator) {
534  array_push($validateAnnotations, [
535  'argumentName' => $parsedAnnotations['argumentName'],
536  'validatorName' => $validator['validatorName'],
537  'validatorOptions' => $validator['validatorOptions']
538  ]);
539  }
540  }
541  }
542 
543  return $validateAnnotations;
544  }
545 }
createValidator($validatorType, array $validatorOptions=[])
addValidator(\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface $validator)
injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
injectReflectionService(\TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService)
buildSubObjectValidator(array $objectPath,\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface $propertyValidator)
static devLog($msg, $extKey, $severity=0, $dataVar=false)
addCustomValidators($targetClassName, ConjunctionValidator &$conjunctionValidator)