Completed
Push — master ( c333ef...77efc7 )
by
unknown
19:20
created

ValidatorResolver::injectObjectManager()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Extbase\Validation;
17
18
use TYPO3\CMS\Core\Log\LogManager;
19
use TYPO3\CMS\Core\SingletonInterface;
20
use TYPO3\CMS\Core\Utility\GeneralUtility;
21
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
22
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
23
use TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException;
24
use TYPO3\CMS\Extbase\Validation\Validator\CollectionValidator;
25
use TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator;
26
use TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator;
27
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
28
29
/**
30
 * Validator resolver to automatically find an appropriate validator for a given subject
31
 * @internal only to be used within Extbase, not part of TYPO3 Core API.
32
 */
33
class ValidatorResolver implements SingletonInterface
34
{
35
    /**
36
     * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
37
     */
38
    protected $reflectionService;
39
40
    /**
41
     * @var array
42
     */
43
    protected $baseValidatorConjunctions = [];
44
45
    /**
46
     * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
47
     */
48
    public function injectReflectionService(ReflectionService $reflectionService)
49
    {
50
        $this->reflectionService = $reflectionService;
51
    }
52
53
    /**
54
     * Get a validator for a given data type. Returns a validator implementing
55
     * the \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface or NULL if no validator
56
     * could be resolved.
57
     *
58
     * @param string $validatorType Either one of the built-in data types or fully qualified validator class name
59
     * @param array $validatorOptions Options to be passed to the validator
60
     * @return \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface Validator or NULL if none found.
61
     */
62
    public function createValidator($validatorType, array $validatorOptions = [])
63
    {
64
        // todo: ValidatorResolver should not take care of creating validator instances.
65
        // todo: Instead, a factory should be used.
66
        try {
67
            /**
68
             * @todo remove throwing Exceptions in resolveValidatorObjectName
69
             */
70
            $validatorObjectName = ValidatorClassNameResolver::resolve($validatorType);
71
72
            $validator = GeneralUtility::makeInstance($validatorObjectName, $validatorOptions);
73
74
            // Move this check into ClassSchema
75
            if (!($validator instanceof ValidatorInterface)) {
76
                throw new NoSuchValidatorException('The validator "' . $validatorObjectName . '" does not implement TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface!', 1300694875);
77
            }
78
79
            return $validator;
80
        } catch (NoSuchValidatorException $e) {
81
            GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__)->debug($e->getMessage());
82
            return null;
83
        }
84
    }
85
86
    /**
87
     * Resolves and returns the base validator conjunction for the given data type.
88
     *
89
     * If no validator could be resolved (which usually means that no validation is necessary),
90
     * NULL is returned.
91
     *
92
     * @param string $targetClassName The data type to search a validator for. Usually the fully qualified object name
93
     * @return ConjunctionValidator The validator conjunction or NULL
94
     */
95
    public function getBaseValidatorConjunction($targetClassName)
96
    {
97
        if (!array_key_exists($targetClassName, $this->baseValidatorConjunctions)) {
98
            $this->buildBaseValidatorConjunction($targetClassName, $targetClassName);
99
        }
100
101
        return $this->baseValidatorConjunctions[$targetClassName];
102
    }
103
104
    /**
105
     * Builds a base validator conjunction for the given data type.
106
     *
107
     * The base validation rules are those which were declared directly in a class (typically
108
     * a model) through some validate annotations on properties.
109
     *
110
     * If a property holds a class for which a base validator exists, that property will be
111
     * checked as well, regardless of a validate annotation
112
     *
113
     * Additionally, if a custom validator was defined for the class in question, it will be added
114
     * to the end of the conjunction. A custom validator is found if it follows the naming convention
115
     * "Replace '\Model\' by '\Validator\' and append 'Validator'".
116
     *
117
     * Example: $targetClassName is TYPO3\Foo\Domain\Model\Quux, then the validator will be found if it has the
118
     * name TYPO3\Foo\Domain\Validator\QuuxValidator
119
     *
120
     * @param string $indexKey The key to use as index in $this->baseValidatorConjunctions; calculated from target class name and validation groups
121
     * @param string $targetClassName The data type to build the validation conjunction for. Needs to be the fully qualified class name.
122
     * @param array $validationGroups The validation groups to build the validator for
123
     * @throws \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException
124
     * @throws \InvalidArgumentException
125
     */
126
    protected function buildBaseValidatorConjunction($indexKey, $targetClassName, array $validationGroups = [])
127
    {
128
        $conjunctionValidator = new ConjunctionValidator();
129
        $this->baseValidatorConjunctions[$indexKey] = $conjunctionValidator;
130
131
        // note: the simpleType check reduces lookups to the class loader
132
        if (!TypeHandlingUtility::isSimpleType($targetClassName) && class_exists($targetClassName)) {
133
            $classSchema = $this->reflectionService->getClassSchema($targetClassName);
134
135
            // Model based validator
136
            /** @var \TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator $objectValidator */
137
            $objectValidator = GeneralUtility::makeInstance(GenericObjectValidator::class, []);
138
            foreach ($classSchema->getProperties() as $property) {
139
                if ($property->getType() === null) {
140
                    // todo: The type is only necessary here for further analyzations whether it's a simple type or
141
                    // todo: a collection. If this is evaluated in the ClassSchema, this whole code part is not needed
142
                    // todo: any longer and can be removed.
143
                    throw new \InvalidArgumentException(sprintf('There is no @var annotation for property "%s" in class "%s".', $property->getName(), $targetClassName), 1363778104);
144
                }
145
146
                $propertyTargetClassName = $property->getType();
147
                // note: the outer simpleType check reduces lookups to the class loader
148
149
                // todo: whether the property holds a simple type or not and whether it holds a collection is known in
150
                // todo: in the ClassSchema. The information could be made available and not evaluated here again.
151
                if (!TypeHandlingUtility::isSimpleType($propertyTargetClassName)) {
152
                    if (TypeHandlingUtility::isCollectionType($propertyTargetClassName)) {
153
                        $collectionValidator = $this->createValidator(
154
                            CollectionValidator::class,
155
                            [
156
                                'elementType' => $property->getElementType(),
157
                                'validationGroups' => $validationGroups
158
                            ]
159
                        );
160
                        $objectValidator->addPropertyValidator($property->getName(), $collectionValidator);
161
                    } elseif (class_exists($propertyTargetClassName) && !TypeHandlingUtility::isCoreType($propertyTargetClassName) && !in_array(SingletonInterface::class, class_implements($propertyTargetClassName, true), true)) {
162
                        /*
163
                         * class_exists($propertyTargetClassName) checks, if the type of the property is an object
164
                         * instead of a simple type. Like DateTime or another model.
165
                         *
166
                         * !TypeHandlingUtility::isCoreType($propertyTargetClassName) checks if the type of the property
167
                         * is not a core type, which are Enums and File objects for example.
168
                         * todo: check why these types should not be validated
169
                         *
170
                         * !in_array(SingletonInterface::class, class_implements($propertyTargetClassName, true), true)
171
                         * checks if the class is an instance of a Singleton
172
                         * todo: check why Singletons shouldn't be validated.
173
                         */
174
175
                        /*
176
                         * (Alexander Schnitzler) By looking at this code I assume that this is the path for 1:1
177
                         * relations in models. Still, the question remains why it excludes core types and singletons.
178
                         * It makes sense on a theoretical level but I don't see a technical issue allowing these as
179
                         * well.
180
                         */
181
                        $validatorForProperty = $this->getBaseValidatorConjunction($propertyTargetClassName);
182
                        if ($validatorForProperty !== null && $validatorForProperty->count() > 0) {
183
                            $objectValidator->addPropertyValidator($property->getName(), $validatorForProperty);
184
                        }
185
                    }
186
                }
187
188
                foreach ($property->getValidators() as $validatorDefinition) {
189
                    // @todo: Respect validationGroups
190
191
                    // @todo: At this point we already have the class name of the validator, thus there is not need
192
                    // @todo: calling \TYPO3\CMS\Extbase\Validation\ValidatorResolver::resolveValidatorObjectName inside
193
                    // @todo: \TYPO3\CMS\Extbase\Validation\ValidatorResolver::createValidator once again. However, to
194
                    // @todo: keep things simple for now, we still use the method createValidator here. In the future,
195
                    // @todo: createValidator must only accept FQCN's.
196
                    $newValidator = $this->createValidator($validatorDefinition['className'], $validatorDefinition['options']);
197
                    if ($newValidator === null) {
198
                        throw new NoSuchValidatorException('Invalid validate annotation in ' . $targetClassName . '::' . $property->getName() . ': Could not resolve class name for validator "' . $validatorDefinition['className'] . '".', 1241098027);
199
                    }
200
                    $objectValidator->addPropertyValidator($property->getName(), $newValidator);
201
                }
202
            }
203
204
            if (!empty($objectValidator->getPropertyValidators())) {
205
                $conjunctionValidator->addValidator($objectValidator);
206
            }
207
        }
208
    }
209
}
210