TYPO3 CMS  TYPO3_8-7
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 
265  protected function buildBaseValidatorConjunction($indexKey, $targetClassName, array $validationGroups = [])
266  {
267  $conjunctionValidator = new ConjunctionValidator();
268  $this->baseValidatorConjunctions[$indexKey] = $conjunctionValidator;
269 
270  // note: the simpleType check reduces lookups to the class loader
271  if (!TypeHandlingUtility::isSimpleType($targetClassName) && class_exists($targetClassName)) {
272  // Model based validator
274  $objectValidator = $this->objectManager->get(\TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator::class, []);
275  foreach ($this->reflectionService->getClassPropertyNames($targetClassName) as $classPropertyName) {
276  $classPropertyTagsValues = $this->reflectionService->getPropertyTagsValues($targetClassName, $classPropertyName);
277 
278  if (!isset($classPropertyTagsValues['var'])) {
279  throw new \InvalidArgumentException(sprintf('There is no @var annotation for property "%s" in class "%s".', $classPropertyName, $targetClassName), 1363778104);
280  }
281  try {
282  $parsedType = TypeHandlingUtility::parseType(trim(implode('', $classPropertyTagsValues['var']), ' \\'));
283  } catch (\TYPO3\CMS\Extbase\Utility\Exception\InvalidTypeException $exception) {
284  throw new \InvalidArgumentException(sprintf(' @var annotation of ' . $exception->getMessage(), 'class "' . $targetClassName . '", property "' . $classPropertyName . '"'), 1315564744, $exception);
285  }
286  $propertyTargetClassName = $parsedType['type'];
287  // note: the outer simpleType check reduces lookups to the class loader
288  if (!TypeHandlingUtility::isSimpleType($propertyTargetClassName)) {
289  if (TypeHandlingUtility::isCollectionType($propertyTargetClassName)) {
290  $collectionValidator = $this->createValidator(\TYPO3\CMS\Extbase\Validation\Validator\CollectionValidator::class, ['elementType' => $parsedType['elementType'], 'validationGroups' => $validationGroups]);
291  $objectValidator->addPropertyValidator($classPropertyName, $collectionValidator);
292  } elseif (class_exists($propertyTargetClassName) && !TypeHandlingUtility::isCoreType($propertyTargetClassName) && $this->objectManager->isRegistered($propertyTargetClassName) && $this->objectManager->getScope($propertyTargetClassName) === \TYPO3\CMS\Extbase\Object\Container\Container::SCOPE_PROTOTYPE) {
293  $validatorForProperty = $this->getBaseValidatorConjunction($propertyTargetClassName);
294  if ($validatorForProperty !== null && $validatorForProperty->count() > 0) {
295  $objectValidator->addPropertyValidator($classPropertyName, $validatorForProperty);
296  }
297  }
298  }
299 
300  $validateAnnotations = [];
301  // @todo: Resolve annotations via reflectionService once its available
302  if (isset($classPropertyTagsValues['validate']) && is_array($classPropertyTagsValues['validate'])) {
303  foreach ($classPropertyTagsValues['validate'] as $validateValue) {
304  $parsedAnnotations = $this->parseValidatorAnnotation($validateValue);
305 
306  foreach ($parsedAnnotations['validators'] as $validator) {
307  array_push($validateAnnotations, [
308  'argumentName' => $parsedAnnotations['argumentName'],
309  'validatorName' => $validator['validatorName'],
310  'validatorOptions' => $validator['validatorOptions']
311  ]);
312  }
313  }
314  }
315 
316  foreach ($validateAnnotations as $validateAnnotation) {
317  // @todo: Respect validationGroups
318  $newValidator = $this->createValidator($validateAnnotation['validatorName'], $validateAnnotation['validatorOptions']);
319  if ($newValidator === null) {
320  throw new Exception\NoSuchValidatorException('Invalid validate annotation in ' . $targetClassName . '::' . $classPropertyName . ': Could not resolve class name for validator "' . $validateAnnotation->type . '".', 1241098027);
321  }
322  $objectValidator->addPropertyValidator($classPropertyName, $newValidator);
323  }
324  }
325 
326  if (!empty($objectValidator->getPropertyValidators())) {
327  $conjunctionValidator->addValidator($objectValidator);
328  }
329  }
330 
331  $this->addCustomValidators($targetClassName, $conjunctionValidator);
332  }
333 
348  protected function addCustomValidators($targetClassName, ConjunctionValidator &$conjunctionValidator)
349  {
350  // @todo: get rid of ClassNamingUtility usage once we dropped underscored class name support
351  $possibleValidatorClassName = ClassNamingUtility::translateModelNameToValidatorName($targetClassName);
352 
353  $customValidator = $this->createValidator($possibleValidatorClassName);
354  if ($customValidator !== null) {
355  $conjunctionValidator->addValidator($customValidator);
356  }
357 
358  // @todo: find polytype validator for class
359  }
360 
367  protected function parseValidatorAnnotation($validateValue)
368  {
369  $matches = [];
370  if ($validateValue[0] === '$') {
371  $parts = explode(' ', $validateValue, 2);
372  $validatorConfiguration = ['argumentName' => ltrim($parts[0], '$'), 'validators' => []];
373  preg_match_all(self::PATTERN_MATCH_VALIDATORS, $parts[1], $matches, PREG_SET_ORDER);
374  } else {
375  $validatorConfiguration = ['validators' => []];
376  preg_match_all(self::PATTERN_MATCH_VALIDATORS, $validateValue, $matches, PREG_SET_ORDER);
377  }
378  foreach ($matches as $match) {
379  $validatorOptions = [];
380  if (isset($match['validatorOptions'])) {
381  $validatorOptions = $this->parseValidatorOptions($match['validatorOptions']);
382  }
383  $validatorConfiguration['validators'][] = ['validatorName' => $match['validatorName'], 'validatorOptions' => $validatorOptions];
384  }
385  return $validatorConfiguration;
386  }
387 
395  protected function parseValidatorOptions($rawValidatorOptions)
396  {
397  $validatorOptions = [];
398  $parsedValidatorOptions = [];
399  preg_match_all(self::PATTERN_MATCH_VALIDATOROPTIONS, $rawValidatorOptions, $validatorOptions, PREG_SET_ORDER);
400  foreach ($validatorOptions as $validatorOption) {
401  $parsedValidatorOptions[trim($validatorOption['optionName'])] = trim($validatorOption['optionValue']);
402  }
403  array_walk($parsedValidatorOptions, [$this, 'unquoteString']);
404  return $parsedValidatorOptions;
405  }
406 
415  protected function unquoteString(&$quotedValue)
416  {
417  switch ($quotedValue[0]) {
418  case '"':
419  $quotedValue = str_replace('\\"', '"', trim($quotedValue, '"'));
420  break;
421  case '\'':
422  $quotedValue = str_replace('\\\'', '\'', trim($quotedValue, '\''));
423  break;
424  }
425  $quotedValue = str_replace('\\\\', '\\', $quotedValue);
426  }
427 
437  protected function resolveValidatorObjectName($validatorName)
438  {
439  if (strpos($validatorName, ':') !== false || strpbrk($validatorName, '_\\') === false) {
440  // Found shorthand validator, either extbase or foreign extension
441  // NotEmpty or Acme.MyPck.Ext:MyValidator
442  list($extensionName, $extensionValidatorName) = explode(':', $validatorName);
443 
444  if ($validatorName !== $extensionName && $extensionValidatorName !== '') {
445  // Shorthand custom
446  if (strpos($extensionName, '.') !== false) {
447  $extensionNameParts = explode('.', $extensionName);
448  $extensionName = array_pop($extensionNameParts);
449  $vendorName = implode('\\', $extensionNameParts);
450  $possibleClassName = $vendorName . '\\' . $extensionName . '\\Validation\\Validator\\' . $extensionValidatorName;
451  } else {
452  $possibleClassName = 'Tx_' . $extensionName . '_Validation_Validator_' . $extensionValidatorName;
453  }
454  } else {
455  // Shorthand built in
456  $possibleClassName = 'TYPO3\\CMS\\Extbase\\Validation\\Validator\\' . $this->getValidatorType($validatorName);
457  }
458  } else {
459  // Full qualified
460  // Tx_MyExt_Validation_Validator_MyValidator or \Acme\Ext\Validation\Validator\FooValidator
461  $possibleClassName = $validatorName;
462  if (!empty($possibleClassName) && $possibleClassName[0] === '\\') {
463  $possibleClassName = substr($possibleClassName, 1);
464  }
465  }
466 
467  if (substr($possibleClassName, - strlen('Validator')) !== 'Validator') {
468  $possibleClassName .= 'Validator';
469  }
470 
471  if (class_exists($possibleClassName)) {
472  $possibleClassNameInterfaces = class_implements($possibleClassName);
473  if (!in_array(\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface::class, $possibleClassNameInterfaces)) {
474  // The guessed validatorname is a valid class name, but does not implement the ValidatorInterface
475  throw new NoSuchValidatorException('Validator class ' . $validatorName . ' must implement \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface', 1365776838);
476  }
477  $resolvedValidatorName = $possibleClassName;
478  } else {
479  throw new NoSuchValidatorException('Validator class ' . $validatorName . ' does not exist', 1365799920);
480  }
481 
482  return $resolvedValidatorName;
483  }
484 
491  protected function getValidatorType($type)
492  {
493  switch ($type) {
494  case 'int':
495  $type = 'Integer';
496  break;
497  case 'bool':
498  $type = 'Boolean';
499  break;
500  case 'double':
501  $type = 'Float';
502  break;
503  case 'numeric':
504  $type = 'Number';
505  break;
506  case 'mixed':
507  $type = 'Raw';
508  break;
509  default:
510  $type = ucfirst($type);
511  }
512  return $type;
513  }
514 
523  public function getMethodValidateAnnotations($className, $methodName)
524  {
525  $validateAnnotations = [];
526  $methodTagsValues = $this->reflectionService->getMethodTagsValues($className, $methodName);
527  if (isset($methodTagsValues['validate']) && is_array($methodTagsValues['validate'])) {
528  foreach ($methodTagsValues['validate'] as $validateValue) {
529  $parsedAnnotations = $this->parseValidatorAnnotation($validateValue);
530 
531  foreach ($parsedAnnotations['validators'] as $validator) {
532  array_push($validateAnnotations, [
533  'argumentName' => $parsedAnnotations['argumentName'],
534  'validatorName' => $validator['validatorName'],
535  'validatorOptions' => $validator['validatorOptions']
536  ]);
537  }
538  }
539  }
540 
541  return $validateAnnotations;
542  }
543 }
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=[])