[!!!][TASK] Remove deprecated Extbase-related code (Part 2)
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Validation / ValidatorResolver.php
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
17 use TYPO3\CMS\Core\Log\LogManager;
18 use TYPO3\CMS\Core\Utility\ClassNamingUtility;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
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 * @internal only to be used within Extbase, not part of TYPO3 Core API.
27 */
28 class ValidatorResolver implements \TYPO3\CMS\Core\SingletonInterface
29 {
30 /**
31 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
32 */
33 protected $objectManager;
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\Object\ObjectManagerInterface $objectManager
47 */
48 public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
49 {
50 $this->objectManager = $objectManager;
51 }
52
53 /**
54 * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
55 */
56 public function injectReflectionService(\TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService)
57 {
58 $this->reflectionService = $reflectionService;
59 }
60
61 /**
62 * Get a validator for a given data type. Returns a validator implementing
63 * the \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface or NULL if no validator
64 * could be resolved.
65 *
66 * @param string $validatorType Either one of the built-in data types or fully qualified validator class name
67 * @param array $validatorOptions Options to be passed to the validator
68 * @return \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface Validator or NULL if none found.
69 */
70 public function createValidator($validatorType, array $validatorOptions = [])
71 {
72 try {
73 /**
74 * @todo remove throwing Exceptions in resolveValidatorObjectName
75 */
76 $validatorObjectName = $this->resolveValidatorObjectName($validatorType);
77
78 $validator = $this->objectManager->get($validatorObjectName, $validatorOptions);
79
80 // Move this check into ClassSchema
81 if (!($validator instanceof \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface)) {
82 throw new Exception\NoSuchValidatorException('The validator "' . $validatorObjectName . '" does not implement TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface!', 1300694875);
83 }
84
85 return $validator;
86 } catch (NoSuchValidatorException $e) {
87 GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__)->debug($e->getMessage());
88 return null;
89 }
90 }
91
92 /**
93 * Resolves and returns the base validator conjunction for the given data type.
94 *
95 * If no validator could be resolved (which usually means that no validation is necessary),
96 * NULL is returned.
97 *
98 * @param string $targetClassName The data type to search a validator for. Usually the fully qualified object name
99 * @return ConjunctionValidator The validator conjunction or NULL
100 */
101 public function getBaseValidatorConjunction($targetClassName)
102 {
103 if (!array_key_exists($targetClassName, $this->baseValidatorConjunctions)) {
104 $this->buildBaseValidatorConjunction($targetClassName, $targetClassName);
105 }
106
107 return $this->baseValidatorConjunctions[$targetClassName];
108 }
109
110 /**
111 * Builds a base validator conjunction for the given data type.
112 *
113 * The base validation rules are those which were declared directly in a class (typically
114 * a model) through some validate annotations on properties.
115 *
116 * If a property holds a class for which a base validator exists, that property will be
117 * checked as well, regardless of a validate annotation
118 *
119 * Additionally, if a custom validator was defined for the class in question, it will be added
120 * to the end of the conjunction. A custom validator is found if it follows the naming convention
121 * "Replace '\Model\' by '\Validator\' and append 'Validator'".
122 *
123 * Example: $targetClassName is TYPO3\Foo\Domain\Model\Quux, then the validator will be found if it has the
124 * name TYPO3\Foo\Domain\Validator\QuuxValidator
125 *
126 * @param string $indexKey The key to use as index in $this->baseValidatorConjunctions; calculated from target class name and validation groups
127 * @param string $targetClassName The data type to build the validation conjunction for. Needs to be the fully qualified class name.
128 * @param array $validationGroups The validation groups to build the validator for
129 * @throws \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException
130 * @throws \InvalidArgumentException
131 */
132 protected function buildBaseValidatorConjunction($indexKey, $targetClassName, array $validationGroups = [])
133 {
134 $conjunctionValidator = new ConjunctionValidator();
135 $this->baseValidatorConjunctions[$indexKey] = $conjunctionValidator;
136
137 // note: the simpleType check reduces lookups to the class loader
138 if (!TypeHandlingUtility::isSimpleType($targetClassName) && class_exists($targetClassName)) {
139 $classSchema = $this->reflectionService->getClassSchema($targetClassName);
140
141 // Model based validator
142 /** @var \TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator $objectValidator */
143 $objectValidator = $this->objectManager->get(\TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator::class, []);
144 foreach ($classSchema->getProperties() as $classPropertyName => $classPropertyDefinition) {
145 /** @var array|array[] $classPropertyDefinition */
146 $classPropertyTagsValues = $classPropertyDefinition['tags'];
147
148 if (!isset($classPropertyTagsValues['var'])) {
149 throw new \InvalidArgumentException(sprintf('There is no @var annotation for property "%s" in class "%s".', $classPropertyName, $targetClassName), 1363778104);
150 }
151
152 $propertyTargetClassName = $classPropertyDefinition['type'];
153 // note: the outer simpleType check reduces lookups to the class loader
154 if (!TypeHandlingUtility::isSimpleType($propertyTargetClassName)) {
155 if (TypeHandlingUtility::isCollectionType($propertyTargetClassName)) {
156 $collectionValidator = $this->createValidator(
157 \TYPO3\CMS\Extbase\Validation\Validator\CollectionValidator::class,
158 [
159 'elementType' => $classPropertyDefinition['elementType'],
160 'validationGroups' => $validationGroups
161 ]
162 );
163 $objectValidator->addPropertyValidator($classPropertyName, $collectionValidator);
164 } elseif (class_exists($propertyTargetClassName) && !TypeHandlingUtility::isCoreType($propertyTargetClassName) && $this->objectManager->isRegistered($propertyTargetClassName) && $this->objectManager->getScope($propertyTargetClassName) === \TYPO3\CMS\Extbase\Object\Container\Container::SCOPE_PROTOTYPE) {
165 $validatorForProperty = $this->getBaseValidatorConjunction($propertyTargetClassName);
166 if ($validatorForProperty !== null && $validatorForProperty->count() > 0) {
167 $objectValidator->addPropertyValidator($classPropertyName, $validatorForProperty);
168 }
169 }
170 }
171
172 foreach ($classPropertyDefinition['validators'] as $validatorDefinition) {
173 // @todo: Respect validationGroups
174
175 // @todo: At this point we already have the class name of the validator, thus there is not need
176 // @todo: calling \TYPO3\CMS\Extbase\Validation\ValidatorResolver::resolveValidatorObjectName inside
177 // @todo: \TYPO3\CMS\Extbase\Validation\ValidatorResolver::createValidator once again. However, to
178 // @todo: keep things simple for now, we still use the method createValidator here. In the future,
179 // @todo: createValidator must only accept FQCN's.
180 $newValidator = $this->createValidator($validatorDefinition['className'], $validatorDefinition['options']);
181 if ($newValidator === null) {
182 throw new Exception\NoSuchValidatorException('Invalid validate annotation in ' . $targetClassName . '::' . $classPropertyName . ': Could not resolve class name for validator "' . $validatorDefinition['className'] . '".', 1241098027);
183 }
184 $objectValidator->addPropertyValidator($classPropertyName, $newValidator);
185 }
186 }
187
188 if (!empty($objectValidator->getPropertyValidators())) {
189 $conjunctionValidator->addValidator($objectValidator);
190 }
191 }
192
193 $this->addCustomValidators($targetClassName, $conjunctionValidator);
194 }
195
196 /**
197 * This adds custom validators to the passed $conjunctionValidator.
198 *
199 * A custom validator is found if it follows the naming convention "Replace '\Model\' by '\Validator\' and
200 * append 'Validator'". If found, it will be added to the $conjunctionValidator.
201 *
202 * In addition canValidate() will be called on all implementations of the ObjectValidatorInterface to find
203 * all validators that could validate the target. The one with the highest priority will be added as well.
204 * If multiple validators have the same priority, which one will be added is not deterministic.
205 *
206 * @param string $targetClassName
207 * @param ConjunctionValidator $conjunctionValidator
208 */
209 protected function addCustomValidators($targetClassName, ConjunctionValidator &$conjunctionValidator)
210 {
211 // @todo: get rid of ClassNamingUtility usage once we dropped underscored class name support
212 $possibleValidatorClassName = ClassNamingUtility::translateModelNameToValidatorName($targetClassName);
213
214 $customValidator = $this->createValidator($possibleValidatorClassName);
215 if ($customValidator !== null) {
216 $conjunctionValidator->addValidator($customValidator);
217 }
218
219 // @todo: find polytype validator for class
220 }
221
222 /**
223 * Returns an object of an appropriate validator for the given class. If no validator is available
224 * FALSE is returned
225 *
226 * @param string $validatorName Either the fully qualified class name of the validator or the short name of a built-in validator
227 *
228 * @throws Exception\NoSuchValidatorException
229 * @return string Name of the validator object
230 * @internal
231 */
232 public function resolveValidatorObjectName($validatorName)
233 {
234 if (strpos($validatorName, ':') !== false) {
235 // Found shorthand validator, either extbase or foreign extension
236 // NotEmpty or Acme.MyPck.Ext:MyValidator
237 list($extensionName, $extensionValidatorName) = explode(':', $validatorName);
238
239 if ($validatorName !== $extensionName && $extensionValidatorName !== '') {
240 // Shorthand custom
241 if (strpos($extensionName, '.') !== false) {
242 $extensionNameParts = explode('.', $extensionName);
243 $extensionName = array_pop($extensionNameParts);
244 $vendorName = implode('\\', $extensionNameParts);
245 $possibleClassName = $vendorName . '\\' . $extensionName . '\\Validation\\Validator\\' . $extensionValidatorName;
246 }
247 } else {
248 // Shorthand built in
249 $possibleClassName = 'TYPO3\\CMS\\Extbase\\Validation\\Validator\\' . $this->getValidatorType($validatorName);
250 }
251 } elseif (strpbrk($validatorName, '\\') === false) {
252 // Shorthand built in
253 $possibleClassName = 'TYPO3\\CMS\\Extbase\\Validation\\Validator\\' . $this->getValidatorType($validatorName);
254 } else {
255 // Full qualified
256 // Example: \Acme\Ext\Validation\Validator\FooValidator
257 $possibleClassName = $validatorName;
258 if (!empty($possibleClassName) && $possibleClassName[0] === '\\') {
259 $possibleClassName = substr($possibleClassName, 1);
260 }
261 }
262
263 if (substr($possibleClassName, - strlen('Validator')) !== 'Validator') {
264 $possibleClassName .= 'Validator';
265 }
266
267 if (class_exists($possibleClassName)) {
268 $possibleClassNameInterfaces = class_implements($possibleClassName);
269 if (!in_array(\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface::class, $possibleClassNameInterfaces)) {
270 // The guessed validatorname is a valid class name, but does not implement the ValidatorInterface
271 throw new NoSuchValidatorException('Validator class ' . $validatorName . ' must implement \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface', 1365776838);
272 }
273 $resolvedValidatorName = $possibleClassName;
274 } else {
275 throw new NoSuchValidatorException('Validator class ' . $validatorName . ' does not exist', 1365799920);
276 }
277
278 return $resolvedValidatorName;
279 }
280
281 /**
282 * Used to map PHP types to validator types.
283 *
284 * @param string $type Data type to unify
285 * @return string unified data type
286 */
287 protected function getValidatorType($type)
288 {
289 switch ($type) {
290 case 'int':
291 $type = 'Integer';
292 break;
293 case 'bool':
294 $type = 'Boolean';
295 break;
296 case 'double':
297 $type = 'Float';
298 break;
299 case 'numeric':
300 $type = 'Number';
301 break;
302 case 'mixed':
303 $type = 'Raw';
304 break;
305 default:
306 $type = ucfirst($type);
307 }
308 return $type;
309 }
310 }