TYPO3 CMS  TYPO3_7-6
ValidatorResolver.php
Go to the documentation of this file.
1 <?php
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  $addedValidatorClassName = null;
352  // @todo: get rid of ClassNamingUtility usage once we dropped underscored class name support
353  $possibleValidatorClassName = ClassNamingUtility::translateModelNameToValidatorName($targetClassName);
354 
355  $customValidator = $this->createValidator($possibleValidatorClassName);
356  if ($customValidator !== null) {
357  $conjunctionValidator->addValidator($customValidator);
358  $addedValidatorClassName = get_class($customValidator);
359  }
360 
361  // @todo: find polytype validator for class
362  }
363 
370  protected function parseValidatorAnnotation($validateValue)
371  {
372  $matches = [];
373  if ($validateValue[0] === '$') {
374  $parts = explode(' ', $validateValue, 2);
375  $validatorConfiguration = ['argumentName' => ltrim($parts[0], '$'), 'validators' => []];
376  preg_match_all(self::PATTERN_MATCH_VALIDATORS, $parts[1], $matches, PREG_SET_ORDER);
377  } else {
378  $validatorConfiguration = ['validators' => []];
379  preg_match_all(self::PATTERN_MATCH_VALIDATORS, $validateValue, $matches, PREG_SET_ORDER);
380  }
381  foreach ($matches as $match) {
382  $validatorOptions = [];
383  if (isset($match['validatorOptions'])) {
384  $validatorOptions = $this->parseValidatorOptions($match['validatorOptions']);
385  }
386  $validatorConfiguration['validators'][] = ['validatorName' => $match['validatorName'], 'validatorOptions' => $validatorOptions];
387  }
388  return $validatorConfiguration;
389  }
390 
398  protected function parseValidatorOptions($rawValidatorOptions)
399  {
400  $validatorOptions = [];
401  $parsedValidatorOptions = [];
402  preg_match_all(self::PATTERN_MATCH_VALIDATOROPTIONS, $rawValidatorOptions, $validatorOptions, PREG_SET_ORDER);
403  foreach ($validatorOptions as $validatorOption) {
404  $parsedValidatorOptions[trim($validatorOption['optionName'])] = trim($validatorOption['optionValue']);
405  }
406  array_walk($parsedValidatorOptions, [$this, 'unquoteString']);
407  return $parsedValidatorOptions;
408  }
409 
419  protected function unquoteString(&$quotedValue)
420  {
421  switch ($quotedValue[0]) {
422  case '"':
423  $quotedValue = str_replace('\\"', '"', trim($quotedValue, '"'));
424  break;
425  case '\'':
426  $quotedValue = str_replace('\\\'', '\'', trim($quotedValue, '\''));
427  break;
428  }
429  $quotedValue = str_replace('\\\\', '\\', $quotedValue);
430  }
431 
441  protected function resolveValidatorObjectName($validatorName)
442  {
443  if (strpos($validatorName, ':') !== false || strpbrk($validatorName, '_\\') === false) {
444  // Found shorthand validator, either extbase or foreign extension
445  // NotEmpty or Acme.MyPck.Ext:MyValidator
446  list($extensionName, $extensionValidatorName) = explode(':', $validatorName);
447 
448  if ($validatorName !== $extensionName && $extensionValidatorName !== '') {
449  // Shorthand custom
450  if (strpos($extensionName, '.') !== false) {
451  $extensionNameParts = explode('.', $extensionName);
452  $extensionName = array_pop($extensionNameParts);
453  $vendorName = implode('\\', $extensionNameParts);
454  $possibleClassName = $vendorName . '\\' . $extensionName . '\\Validation\\Validator\\' . $extensionValidatorName;
455  } else {
456  $possibleClassName = 'Tx_' . $extensionName . '_Validation_Validator_' . $extensionValidatorName;
457  }
458  } else {
459  // Shorthand built in
460  $possibleClassName = 'TYPO3\\CMS\\Extbase\\Validation\\Validator\\' . $this->getValidatorType($validatorName);
461  }
462  } else {
463  // Full qualified
464  // Tx_MyExt_Validation_Validator_MyValidator or \Acme\Ext\Validation\Validator\FooValidator
465  $possibleClassName = $validatorName;
466  if (!empty($possibleClassName) && $possibleClassName[0] === '\\') {
467  $possibleClassName = substr($possibleClassName, 1);
468  }
469  }
470 
471  if (substr($possibleClassName, - strlen('Validator')) !== 'Validator') {
472  $possibleClassName .= 'Validator';
473  }
474 
475  if (class_exists($possibleClassName)) {
476  $possibleClassNameInterfaces = class_implements($possibleClassName);
477  if (!in_array(\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface::class, $possibleClassNameInterfaces)) {
478  // The guessed validatorname is a valid class name, but does not implement the ValidatorInterface
479  throw new NoSuchValidatorException('Validator class ' . $validatorName . ' must implement \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface', 1365776838);
480  }
481  $resolvedValidatorName = $possibleClassName;
482  } else {
483  throw new NoSuchValidatorException('Validator class ' . $validatorName . ' does not exist', 1365799920);
484  }
485 
486  return $resolvedValidatorName;
487  }
488 
495  protected function getValidatorType($type)
496  {
497  switch ($type) {
498  case 'int':
499  $type = 'Integer';
500  break;
501  case 'bool':
502  $type = 'Boolean';
503  break;
504  case 'double':
505  $type = 'Float';
506  break;
507  case 'numeric':
508  $type = 'Number';
509  break;
510  case 'mixed':
511  $type = 'Raw';
512  break;
513  default:
514  $type = ucfirst($type);
515  }
516  return $type;
517  }
518 
527  public function getMethodValidateAnnotations($className, $methodName)
528  {
529  $validateAnnotations = [];
530  $methodTagsValues = $this->reflectionService->getMethodTagsValues($className, $methodName);
531  if (isset($methodTagsValues['validate']) && is_array($methodTagsValues['validate'])) {
532  foreach ($methodTagsValues['validate'] as $validateValue) {
533  $parsedAnnotations = $this->parseValidatorAnnotation($validateValue);
534 
535  foreach ($parsedAnnotations['validators'] as $validator) {
536  array_push($validateAnnotations, [
537  'argumentName' => $parsedAnnotations['argumentName'],
538  'validatorName' => $validator['validatorName'],
539  'validatorOptions' => $validator['validatorOptions']
540  ]);
541  }
542  }
543  }
544 
545  return $validateAnnotations;
546  }
547 }
static devLog($msg, $extKey, $severity=0, $dataVar=false)
addCustomValidators($targetClassName, ConjunctionValidator &$conjunctionValidator)
buildSubObjectValidator(array $objectPath, \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface $propertyValidator)
injectReflectionService(\TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService)
injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
addValidator(\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface $validator)
createValidator($validatorType, array $validatorOptions=[])