ValidatorResolver   F
last analyzed

Complexity

Total Complexity 75

Size/Duplication

Total Lines 517
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
wmc 75
lcom 1
cbo 7
dl 0
loc 517
rs 2.4
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getBaseValidatorConjunction() 0 8 2
C buildMethodArgumentsValidatorConjunctions() 0 62 12
A buildSubObjectValidator() 0 16 2
D buildBaseValidatorConjunction() 0 68 21
A addCustomValidators() 0 12 2
A parseValidatorAnnotation() 0 20 4
A parseValidatorOptions() 0 11 2
A unquoteString() 0 12 3
B resolveValidatorObjectName() 0 47 11
B getValidatorType() 0 23 6
A getMethodValidateAnnotations() 0 20 5
A injectObjectManager() 0 4 1
A injectReflectionService() 0 4 1
A createValidator() 0 19 3

How to fix   Complexity   

Complex Class

Complex classes like ValidatorResolver often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ValidatorResolver, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Romm\ConfigurationObject\Legacy\Validation;
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
use Romm\ConfigurationObject\Legacy\Reflection\ReflectionService;
19
use TYPO3\CMS\Core\Utility\ClassNamingUtility;
20
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
21
use TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException;
22
use TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator;
23
24
/**
25
 * Validator resolver to automatically find an appropriate validator for a given subject
26
 */
27
class ValidatorResolver implements \TYPO3\CMS\Core\SingletonInterface
28
{
29
    /**
30
     * Match validator names and options
31
     * @todo: adjust [a-z0-9_:.\\\\] once Tx_Extbase_Foo syntax is outdated.
32
     *
33
     * @var string
34
     */
35
    const PATTERN_MATCH_VALIDATORS = '/
36
			(?:^|,\s*)
37
			(?P<validatorName>[a-z0-9_:.\\\\]+)
38
			\s*
39
			(?:\(
40
				(?P<validatorOptions>(?:\s*[a-z0-9]+\s*=\s*(?:
41
					"(?:\\\\"|[^"])*"
42
					|\'(?:\\\\\'|[^\'])*\'
43
					|(?:\s|[^,"\']*)
44
				)(?:\s|,)*)*)
45
			\))?
46
		/ixS';
47
48
    /**
49
     * Match validator options (to parse actual options)
50
     * @var string
51
     */
52
    const PATTERN_MATCH_VALIDATOROPTIONS = '/
53
			\s*
54
			(?P<optionName>[a-z0-9]+)
55
			\s*=\s*
56
			(?P<optionValue>
57
				"(?:\\\\"|[^"])*"
58
				|\'(?:\\\\\'|[^\'])*\'
59
				|(?:\s|[^,"\']*)
60
			)
61
		/ixS';
62
63
    /**
64
     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
65
     */
66
    protected $objectManager;
67
68
    /**
69
     * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
70
     */
71
    protected $reflectionService;
72
73
    /**
74
     * @var array
75
     */
76
    protected $baseValidatorConjunctions = [];
77
78
    /**
79
     * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
80
     */
81
    public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
82
    {
83
        $this->objectManager = $objectManager;
84
    }
85
86
    /**
87
     * @param ReflectionService $reflectionService
88
     */
89
    public function injectReflectionService(ReflectionService $reflectionService)
90
    {
91
        $this->reflectionService = $reflectionService;
0 ignored issues
show
Documentation Bug introduced by
$reflectionService is of type object<Romm\Configuratio...tion\ReflectionService>, but the property $reflectionService was declared to be of type object<TYPO3\CMS\Extbase...tion\ReflectionService>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
92
    }
93
94
    /**
95
     * Get a validator for a given data type. Returns a validator implementing
96
     * the \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface or NULL if no validator
97
     * could be resolved.
98
     *
99
     * @param string $validatorType Either one of the built-in data types or fully qualified validator class name
100
     * @param array $validatorOptions Options to be passed to the validator
101
     * @return \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface Validator or NULL if none found.
102
     */
103
    public function createValidator($validatorType, array $validatorOptions = [])
104
    {
105
        try {
106
            /**
107
             * @todo remove throwing Exceptions in resolveValidatorObjectName
108
             */
109
            $validatorObjectName = $this->resolveValidatorObjectName($validatorType);
110
111
            $validator = $this->objectManager->get($validatorObjectName, $validatorOptions);
112
113
            if (!($validator instanceof \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface)) {
114
                throw new Exception\NoSuchValidatorException('The validator "' . $validatorObjectName . '" does not implement TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface!', 1300694875);
115
            }
116
117
            return $validator;
118
        } catch (NoSuchValidatorException $e) {
119
            return null;
120
        }
121
    }
122
123
    /**
124
     * Resolves and returns the base validator conjunction for the given data type.
125
     *
126
     * If no validator could be resolved (which usually means that no validation is necessary),
127
     * NULL is returned.
128
     *
129
     * @param string $targetClassName The data type to search a validator for. Usually the fully qualified object name
130
     * @return ConjunctionValidator The validator conjunction or NULL
131
     */
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
141
    /**
142
     * Detects and registers any validators for arguments:
143
     * - by the data type specified in the param annotations
144
     * - additional validators specified in the validate annotations of a method
145
     *
146
     * @param string $className
147
     * @param string $methodName
148
     * @param array $methodParameters Optional pre-compiled array of method parameters
149
     * @param array $methodValidateAnnotations Optional pre-compiled array of validate annotations (as array)
150
     * @return ConjunctionValidator[] An Array of ValidatorConjunctions for each method parameters.
151
     * @throws \TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationConfigurationException
152
     * @throws \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException
153
     * @throws \TYPO3\CMS\Extbase\Validation\Exception\InvalidTypeHintException
154
     */
155
    public function buildMethodArgumentsValidatorConjunctions($className, $methodName, array $methodParameters = null, array $methodValidateAnnotations = null)
156
    {
157
        /** @var ConjunctionValidator[] $validatorConjunctions */
158
        $validatorConjunctions = [];
159
160
        if ($methodParameters === null) {
161
            $methodParameters = $this->reflectionService->getMethodParameters($className, $methodName);
0 ignored issues
show
Deprecated Code introduced by
The method TYPO3\CMS\Extbase\Reflec...::getMethodParameters() has been deprecated with message: since TYPO3 v9, will be removed in TYPO3 v10.0.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
162
        }
163
        if (empty($methodParameters)) {
164
            return $validatorConjunctions;
165
        }
166
167
        foreach ($methodParameters as $parameterName => $methodParameter) {
168
            /** @var ConjunctionValidator $validatorConjunction */
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
218
    /**
219
     * Builds a chain of nested object validators by specification of the given
220
     * object path.
221
     *
222
     * @param array $objectPath The object path
223
     * @param \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface $propertyValidator The validator which should be added to the property specified by objectPath
224
     * @return \TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator
225
     */
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
243
    /**
244
     * Builds a base validator conjunction for the given data type.
245
     *
246
     * The base validation rules are those which were declared directly in a class (typically
247
     * a model) through some validate annotations on properties.
248
     *
249
     * If a property holds a class for which a base validator exists, that property will be
250
     * checked as well, regardless of a validate annotation
251
     *
252
     * Additionally, if a custom validator was defined for the class in question, it will be added
253
     * to the end of the conjunction. A custom validator is found if it follows the naming convention
254
     * "Replace '\Model\' by '\Validator\' and append 'Validator'".
255
     *
256
     * Example: $targetClassName is TYPO3\Foo\Domain\Model\Quux, then the validator will be found if it has the
257
     * name TYPO3\Foo\Domain\Validator\QuuxValidator
258
     *
259
     * @param string $indexKey The key to use as index in $this->baseValidatorConjunctions; calculated from target class name and validation groups
260
     * @param string $targetClassName The data type to build the validation conjunction for. Needs to be the fully qualified class name.
261
     * @param array $validationGroups The validation groups to build the validator for
262
     * @throws \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException
263
     * @throws \InvalidArgumentException
264
     */
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
273
            /** @var \TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator $objectValidator */
274
            $objectValidator = $this->objectManager->get(\TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator::class, []);
275
            foreach ($this->reflectionService->getClassPropertyNames($targetClassName) as $classPropertyName) {
0 ignored issues
show
Deprecated Code introduced by
The method TYPO3\CMS\Extbase\Reflec...getClassPropertyNames() has been deprecated with message: since TYPO3 v9, will be removed in TYPO3 v10.0.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
276
                $classPropertyTagsValues = $this->reflectionService->getPropertyTagsValues($targetClassName, $classPropertyName);
0 ignored issues
show
Deprecated Code introduced by
The method TYPO3\CMS\Extbase\Reflec...getPropertyTagsValues() has been deprecated with message: since TYPO3 v9, will be removed in TYPO3 v10.0.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $collectionValidator defined by $this->createValidator(\... => $validationGroups)) on line 290 can be null; however, TYPO3\CMS\Extbase\Valida...:addPropertyValidator() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
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) {
0 ignored issues
show
Bug introduced by
The expression $parsedAnnotations['validators'] of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
307
                            array_push($validateAnnotations, [
308
                                'argumentName' => $parsedAnnotations['argumentName'] ?? null,
309
                                'validatorName' => $validator['validatorName'] ?? null,
310
                                'validatorOptions' => $validator['validatorOptions'] ?? null
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
334
    /**
335
     * This adds custom validators to the passed $conjunctionValidator.
336
     *
337
     * A custom validator is found if it follows the naming convention "Replace '\Model\' by '\Validator\' and
338
     * append 'Validator'". If found, it will be added to the $conjunctionValidator.
339
     *
340
     * In addition canValidate() will be called on all implementations of the ObjectValidatorInterface to find
341
     * all validators that could validate the target. The one with the highest priority will be added as well.
342
     * If multiple validators have the same priority, which one will be added is not deterministic.
343
     *
344
     * @param string $targetClassName
345
     * @param ConjunctionValidator $conjunctionValidator
346
     * @return Validator\ObjectValidatorInterface|null
347
     */
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
361
    /**
362
     * Parses the validator options given in @validate annotations.
363
     *
364
     * @param string $validateValue
365
     * @return array
366
     */
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) {
0 ignored issues
show
Bug introduced by
The expression $matches of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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
388
    /**
389
     * Parses $rawValidatorOptions not containing quoted option values.
390
     * $rawValidatorOptions will be an empty string afterwards (pass by ref!).
391
     *
392
     * @param string $rawValidatorOptions
393
     * @return array An array of optionName/optionValue pairs
394
     */
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) {
0 ignored issues
show
Bug introduced by
The expression $validatorOptions of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
401
            $parsedValidatorOptions[trim($validatorOption['optionName'])] = trim($validatorOption['optionValue']);
402
        }
403
        array_walk($parsedValidatorOptions, [$this, 'unquoteString']);
404
        return $parsedValidatorOptions;
405
    }
406
407
    /**
408
     * Removes escapings from a given argument string and trims the outermost
409
     * quotes.
410
     *
411
     * This method is meant as a helper for regular expression results.
412
     *
413
     * @param string &$quotedValue Value to unquote
414
     */
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
428
    /**
429
     * Returns an object of an appropriate validator for the given class. If no validator is available
430
     * FALSE is returned
431
     *
432
     * @param string $validatorName Either the fully qualified class name of the validator or the short name of a built-in validator
433
     *
434
     * @throws Exception\NoSuchValidatorException
435
     * @return string Name of the validator object
436
     */
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
485
    /**
486
     * Used to map PHP types to validator types.
487
     *
488
     * @param string $type Data type to unify
489
     * @return string unified data type
490
     */
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
515
    /**
516
     * Temporary replacement for $this->reflectionService->getMethodAnnotations()
517
     *
518
     * @param string $className
519
     * @param string $methodName
520
     *
521
     * @return array
522
     */
523
    public function getMethodValidateAnnotations($className, $methodName)
524
    {
525
        $validateAnnotations = [];
526
        $methodTagsValues = $this->reflectionService->getMethodTagsValues($className, $methodName);
0 ignored issues
show
Deprecated Code introduced by
The method TYPO3\CMS\Extbase\Reflec...::getMethodTagsValues() has been deprecated with message: since TYPO3 v9, will be removed in TYPO3 v10.0.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
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) {
0 ignored issues
show
Bug introduced by
The expression $parsedAnnotations['validators'] of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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
}
544