[!!!][TASK] Remove deprecated Extbase-related code (Part 2)
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Reflection / ClassSchema.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Reflection;
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 Doctrine\Common\Annotations\AnnotationReader;
18 use TYPO3\CMS\Core\SingletonInterface;
19 use TYPO3\CMS\Core\Utility\ClassNamingUtility;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21 use TYPO3\CMS\Core\Utility\StringUtility;
22 use TYPO3\CMS\Extbase\Annotation\IgnoreValidation;
23 use TYPO3\CMS\Extbase\Annotation\Inject;
24 use TYPO3\CMS\Extbase\Annotation\ORM\Cascade;
25 use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
26 use TYPO3\CMS\Extbase\Annotation\ORM\Transient;
27 use TYPO3\CMS\Extbase\Annotation\Validate;
28 use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
29 use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
30 use TYPO3\CMS\Extbase\Mvc\Controller\ControllerInterface;
31 use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
32 use TYPO3\CMS\Extbase\Validation\Exception\InvalidTypeHintException;
33 use TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationConfigurationException;
34 use TYPO3\CMS\Extbase\Validation\ValidatorResolver;
35
36 /**
37 * A class schema
38 * @internal only to be used within Extbase, not part of TYPO3 Core API.
39 */
40 class ClassSchema
41 {
42 /**
43 * Available model types
44 */
45 const MODELTYPE_ENTITY = 1;
46 const MODELTYPE_VALUEOBJECT = 2;
47
48 /**
49 * Name of the class this schema is referring to
50 *
51 * @var string
52 */
53 protected $className;
54
55 /**
56 * Model type of the class this schema is referring to
57 *
58 * @var int
59 */
60 protected $modelType = self::MODELTYPE_ENTITY;
61
62 /**
63 * Whether a repository exists for the class this schema is referring to
64 *
65 * @var bool
66 */
67 protected $aggregateRoot = false;
68
69 /**
70 * Properties of the class which need to be persisted
71 *
72 * @var array
73 */
74 protected $properties = [];
75
76 /**
77 * Indicates if the class is a singleton or not.
78 *
79 * @var bool
80 */
81 private $isSingleton;
82
83 /**
84 * @var bool
85 */
86 private $isController;
87
88 /**
89 * @var array
90 */
91 private $methods;
92
93 /**
94 * @var array
95 */
96 private $tags;
97
98 /**
99 * @var array
100 */
101 private $injectProperties = [];
102
103 /**
104 * @var array
105 */
106 private $injectMethods = [];
107
108 /**
109 * Constructs this class schema
110 *
111 * @param string $className Name of the class this schema is referring to
112 * @throws \TYPO3\CMS\Extbase\Reflection\Exception\UnknownClassException
113 * @throws \ReflectionException
114 */
115 public function __construct($className)
116 {
117 $this->className = $className;
118
119 $reflectionClass = new \ReflectionClass($className);
120
121 $this->isSingleton = $reflectionClass->implementsInterface(SingletonInterface::class);
122 $this->isController = $reflectionClass->implementsInterface(ControllerInterface::class);
123
124 if ($reflectionClass->isSubclassOf(AbstractEntity::class)) {
125 $this->modelType = static::MODELTYPE_ENTITY;
126
127 $possibleRepositoryClassName = ClassNamingUtility::translateModelNameToRepositoryName($className);
128 if (class_exists($possibleRepositoryClassName)) {
129 $this->setAggregateRoot(true);
130 }
131 }
132
133 if ($reflectionClass->isSubclassOf(AbstractValueObject::class)) {
134 $this->modelType = static::MODELTYPE_VALUEOBJECT;
135 }
136
137 $docCommentParser = new DocCommentParser(true);
138 $docCommentParser->parseDocComment($reflectionClass->getDocComment());
139 $this->tags = $docCommentParser->getTagsValues();
140
141 $this->reflectProperties($reflectionClass);
142 $this->reflectMethods($reflectionClass);
143 }
144
145 /**
146 * @param \ReflectionClass $reflectionClass
147 */
148 protected function reflectProperties(\ReflectionClass $reflectionClass)
149 {
150 $annotationReader = new AnnotationReader();
151
152 foreach ($reflectionClass->getProperties() as $reflectionProperty) {
153 $propertyName = $reflectionProperty->getName();
154
155 $this->properties[$propertyName] = [
156 'default' => $reflectionProperty->isDefault(),
157 'private' => $reflectionProperty->isPrivate(),
158 'protected' => $reflectionProperty->isProtected(),
159 'public' => $reflectionProperty->isPublic(),
160 'static' => $reflectionProperty->isStatic(),
161 'type' => null, // Extbase
162 'elementType' => null, // Extbase
163 'annotations' => [],
164 'tags' => [],
165 'validators' => []
166 ];
167
168 $docCommentParser = new DocCommentParser(true);
169 $docCommentParser->parseDocComment($reflectionProperty->getDocComment());
170 foreach ($docCommentParser->getTagsValues() as $tag => $values) {
171 $this->properties[$propertyName]['tags'][strtolower($tag)] = $values;
172 }
173
174 $this->properties[$propertyName]['annotations']['inject'] = false;
175 $this->properties[$propertyName]['annotations']['lazy'] = false;
176 $this->properties[$propertyName]['annotations']['transient'] = false;
177 $this->properties[$propertyName]['annotations']['type'] = null;
178 $this->properties[$propertyName]['annotations']['cascade'] = null;
179 $this->properties[$propertyName]['annotations']['dependency'] = null;
180
181 $annotations = $annotationReader->getPropertyAnnotations($reflectionProperty);
182
183 /** @var array|Validate[] $validateAnnotations */
184 $validateAnnotations = array_filter($annotations, function ($annotation) {
185 return $annotation instanceof Validate;
186 });
187
188 if (count($validateAnnotations) > 0) {
189 $validatorResolver = GeneralUtility::makeInstance(ValidatorResolver::class);
190
191 foreach ($validateAnnotations as $validateAnnotation) {
192 $validatorObjectName = $validatorResolver->resolveValidatorObjectName($validateAnnotation->validator);
193
194 $this->properties[$propertyName]['validators'][] = [
195 'name' => $validateAnnotation->validator,
196 'options' => $validateAnnotation->options,
197 'className' => $validatorObjectName,
198 ];
199 }
200 }
201
202 if ($annotationReader->getPropertyAnnotation($reflectionProperty, Lazy::class) instanceof Lazy) {
203 $this->properties[$propertyName]['annotations']['lazy'] = true;
204 }
205
206 if ($annotationReader->getPropertyAnnotation($reflectionProperty, Transient::class) instanceof Transient) {
207 $this->properties[$propertyName]['annotations']['transient'] = true;
208 }
209
210 if ($propertyName !== 'settings'
211 && ($annotationReader->getPropertyAnnotation($reflectionProperty, Inject::class) instanceof Inject)
212 ) {
213 try {
214 $varValue = ltrim($docCommentParser->getTagValues('var')[0], '\\');
215 $this->properties[$propertyName]['annotations']['inject'] = true;
216 $this->properties[$propertyName]['annotations']['type'] = $varValue;
217 $this->properties[$propertyName]['annotations']['dependency'] = $varValue;
218
219 $this->injectProperties[] = $propertyName;
220 } catch (\Exception $e) {
221 }
222 }
223
224 if ($docCommentParser->isTaggedWith('var') && $this->properties[$propertyName]['annotations']['transient'] === false) {
225 if (($annotation = $annotationReader->getPropertyAnnotation($reflectionProperty, Cascade::class)) instanceof Cascade) {
226 /** @var Cascade $annotation */
227 $this->properties[$propertyName]['annotations']['cascade'] = $annotation->value;
228 }
229
230 try {
231 $type = TypeHandlingUtility::parseType(implode(' ', $docCommentParser->getTagValues('var')));
232 } catch (\Exception $e) {
233 $type = [
234 'type' => null,
235 'elementType' => null
236 ];
237 }
238
239 $this->properties[$propertyName]['type'] = $type['type'] ? ltrim($type['type'], '\\') : null;
240 $this->properties[$propertyName]['elementType'] = $type['elementType'] ? ltrim($type['elementType'], '\\') : null;
241 }
242 }
243 }
244
245 /**
246 * @param \ReflectionClass $reflectionClass
247 */
248 protected function reflectMethods(\ReflectionClass $reflectionClass)
249 {
250 $annotationReader = new AnnotationReader();
251
252 foreach ($reflectionClass->getMethods() as $reflectionMethod) {
253 $methodName = $reflectionMethod->getName();
254
255 $this->methods[$methodName] = [];
256 $this->methods[$methodName]['private'] = $reflectionMethod->isPrivate();
257 $this->methods[$methodName]['protected'] = $reflectionMethod->isProtected();
258 $this->methods[$methodName]['public'] = $reflectionMethod->isPublic();
259 $this->methods[$methodName]['static'] = $reflectionMethod->isStatic();
260 $this->methods[$methodName]['abstract'] = $reflectionMethod->isAbstract();
261 $this->methods[$methodName]['params'] = [];
262 $this->methods[$methodName]['tags'] = [];
263 $this->methods[$methodName]['annotations'] = [];
264 $this->methods[$methodName]['isAction'] = StringUtility::endsWith($methodName, 'Action');
265
266 $docCommentParser = new DocCommentParser(true);
267 $docCommentParser->parseDocComment($reflectionMethod->getDocComment());
268
269 $argumentValidators = [];
270
271 $annotations = $annotationReader->getMethodAnnotations($reflectionMethod);
272
273 /** @var array|Validate[] $validateAnnotations */
274 $validateAnnotations = array_filter($annotations, function ($annotation) {
275 return $annotation instanceof Validate;
276 });
277
278 if ($this->isController && $this->methods[$methodName]['isAction'] && count($validateAnnotations) > 0) {
279 $validatorResolver = GeneralUtility::makeInstance(ValidatorResolver::class);
280
281 foreach ($validateAnnotations as $validateAnnotation) {
282 $validatorName = $validateAnnotation->validator;
283 $validatorObjectName = $validatorResolver->resolveValidatorObjectName($validatorName);
284
285 $argumentValidators[$validateAnnotation->param][] = [
286 'name' => $validatorName,
287 'options' => $validateAnnotation->options,
288 'className' => $validatorObjectName,
289 ];
290 }
291 }
292
293 foreach ($docCommentParser->getTagsValues() as $tag => $values) {
294 $this->methods[$methodName]['tags'][$tag] = array_map(function ($value) {
295 return ltrim($value, '$');
296 }, $values);
297 }
298
299 foreach ($annotations as $annotation) {
300 if ($annotation instanceof IgnoreValidation) {
301 $this->methods[$methodName]['tags']['ignorevalidation'][] = $annotation->argumentName;
302 }
303 }
304
305 $this->methods[$methodName]['description'] = $docCommentParser->getDescription();
306
307 foreach ($reflectionMethod->getParameters() as $parameterPosition => $reflectionParameter) {
308 /* @var \ReflectionParameter $reflectionParameter */
309
310 $parameterName = $reflectionParameter->getName();
311
312 $this->methods[$methodName]['params'][$parameterName] = [];
313 $this->methods[$methodName]['params'][$parameterName]['position'] = $parameterPosition; // compat
314 $this->methods[$methodName]['params'][$parameterName]['byReference'] = $reflectionParameter->isPassedByReference(); // compat
315 $this->methods[$methodName]['params'][$parameterName]['array'] = $reflectionParameter->isArray(); // compat
316 $this->methods[$methodName]['params'][$parameterName]['optional'] = $reflectionParameter->isOptional();
317 $this->methods[$methodName]['params'][$parameterName]['allowsNull'] = $reflectionParameter->allowsNull(); // compat
318 $this->methods[$methodName]['params'][$parameterName]['class'] = null; // compat
319 $this->methods[$methodName]['params'][$parameterName]['type'] = null;
320 $this->methods[$methodName]['params'][$parameterName]['nullable'] = $reflectionParameter->allowsNull();
321 $this->methods[$methodName]['params'][$parameterName]['default'] = null;
322 $this->methods[$methodName]['params'][$parameterName]['hasDefaultValue'] = $reflectionParameter->isDefaultValueAvailable();
323 $this->methods[$methodName]['params'][$parameterName]['defaultValue'] = null; // compat
324 $this->methods[$methodName]['params'][$parameterName]['dependency'] = null; // Extbase DI
325 $this->methods[$methodName]['params'][$parameterName]['validators'] = [];
326
327 if ($reflectionParameter->isDefaultValueAvailable()) {
328 $this->methods[$methodName]['params'][$parameterName]['default'] = $reflectionParameter->getDefaultValue();
329 $this->methods[$methodName]['params'][$parameterName]['defaultValue'] = $reflectionParameter->getDefaultValue(); // compat
330 }
331
332 if (($reflectionType = $reflectionParameter->getType()) instanceof \ReflectionType) {
333 $this->methods[$methodName]['params'][$parameterName]['type'] = (string)$reflectionType;
334 $this->methods[$methodName]['params'][$parameterName]['nullable'] = $reflectionType->allowsNull();
335 }
336
337 if (($parameterClass = $reflectionParameter->getClass()) instanceof \ReflectionClass) {
338 $this->methods[$methodName]['params'][$parameterName]['class'] = $parameterClass->getName();
339 $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($parameterClass->getName(), '\\');
340 } else {
341 $methodTagsAndValues = $this->methods[$methodName]['tags'];
342 if (isset($methodTagsAndValues['param'][$parameterPosition])) {
343 $explodedParameters = explode(' ', $methodTagsAndValues['param'][$parameterPosition]);
344 if (count($explodedParameters) >= 2) {
345 if (TypeHandlingUtility::isSimpleType($explodedParameters[0])) {
346 // ensure that short names of simple types are resolved correctly to the long form
347 // this is important for all kinds of type checks later on
348 $typeInfo = TypeHandlingUtility::parseType($explodedParameters[0]);
349
350 $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($typeInfo['type'], '\\');
351 } else {
352 $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($explodedParameters[0], '\\');
353 }
354 }
355 }
356 }
357
358 // Extbase DI
359 if ($reflectionParameter->getClass() instanceof \ReflectionClass
360 && ($reflectionMethod->isConstructor() || $this->hasInjectMethodName($reflectionMethod))
361 ) {
362 $this->methods[$methodName]['params'][$parameterName]['dependency'] = $reflectionParameter->getClass()->getName();
363 }
364
365 // Extbase Validation
366 if (isset($argumentValidators[$parameterName])) {
367 if ($this->methods[$methodName]['params'][$parameterName]['type'] === null) {
368 throw new InvalidTypeHintException(
369 'Missing type information for parameter "$' . $parameterName . '" in ' . $this->className . '->' . $methodName . '(): Either use an @param annotation or use a type hint.',
370 1515075192
371 );
372 }
373
374 $this->methods[$methodName]['params'][$parameterName]['validators'] = $argumentValidators[$parameterName];
375 unset($argumentValidators[$parameterName]);
376 }
377 }
378
379 // Extbase Validation
380 foreach ($argumentValidators as $parameterName => $validators) {
381 $validatorNames = array_column($validators, 'name');
382
383 throw new InvalidValidationConfigurationException(
384 'Invalid validate annotation in ' . $this->className . '->' . $methodName . '(): The following validators have been defined for missing param "$' . $parameterName . '": ' . implode(', ', $validatorNames),
385 1515073585
386 );
387 }
388
389 // Extbase
390 $this->methods[$methodName]['injectMethod'] = false;
391 if ($this->hasInjectMethodName($reflectionMethod)
392 && count($this->methods[$methodName]['params']) === 1
393 && reset($this->methods[$methodName]['params'])['dependency'] !== null
394 ) {
395 $this->methods[$methodName]['injectMethod'] = true;
396 $this->injectMethods[] = $methodName;
397 }
398 }
399 }
400
401 /**
402 * Returns the class name this schema is referring to
403 *
404 * @return string The class name
405 */
406 public function getClassName(): string
407 {
408 return $this->className;
409 }
410
411 /**
412 * Returns the given property defined in this schema. Check with
413 * hasProperty($propertyName) before!
414 *
415 * @param string $propertyName
416 * @return array
417 */
418 public function getProperty($propertyName)
419 {
420 return isset($this->properties[$propertyName]) && is_array($this->properties[$propertyName])
421 ? $this->properties[$propertyName]
422 : [];
423 }
424
425 /**
426 * Returns all properties defined in this schema
427 *
428 * @return array
429 */
430 public function getProperties()
431 {
432 return $this->properties;
433 }
434
435 /**
436 * Marks the class if it is root of an aggregate and therefore accessible
437 * through a repository - or not.
438 *
439 * @param bool $isRoot TRUE if it is the root of an aggregate
440 */
441 public function setAggregateRoot($isRoot)
442 {
443 $this->aggregateRoot = $isRoot;
444 }
445
446 /**
447 * Whether the class is an aggregate root and therefore accessible through
448 * a repository.
449 *
450 * @return bool TRUE if it is managed
451 */
452 public function isAggregateRoot(): bool
453 {
454 return $this->aggregateRoot;
455 }
456
457 /**
458 * If the class schema has a certain property.
459 *
460 * @param string $propertyName Name of the property
461 * @return bool
462 */
463 public function hasProperty($propertyName): bool
464 {
465 return array_key_exists($propertyName, $this->properties);
466 }
467
468 /**
469 * @return bool
470 */
471 public function hasConstructor(): bool
472 {
473 return isset($this->methods['__construct']);
474 }
475
476 /**
477 * @param string $name
478 * @return array
479 */
480 public function getMethod(string $name): array
481 {
482 return $this->methods[$name] ?? [];
483 }
484
485 /**
486 * @return array
487 */
488 public function getMethods(): array
489 {
490 return $this->methods;
491 }
492
493 /**
494 * @param \ReflectionMethod $reflectionMethod
495 * @return bool
496 */
497 protected function hasInjectMethodName(\ReflectionMethod $reflectionMethod): bool
498 {
499 $methodName = $reflectionMethod->getName();
500 if ($methodName === 'injectSettings' || !$reflectionMethod->isPublic()) {
501 return false;
502 }
503
504 if (
505 strpos($reflectionMethod->getName(), 'inject') === 0
506 ) {
507 return true;
508 }
509
510 return false;
511 }
512
513 /**
514 * @return bool
515 * @internal
516 */
517 public function isModel(): bool
518 {
519 return $this->isEntity() || $this->isValueObject();
520 }
521
522 /**
523 * @return bool
524 * @internal
525 */
526 public function isEntity(): bool
527 {
528 return $this->modelType === static::MODELTYPE_ENTITY;
529 }
530
531 /**
532 * @return bool
533 * @internal
534 */
535 public function isValueObject(): bool
536 {
537 return $this->modelType === static::MODELTYPE_VALUEOBJECT;
538 }
539
540 /**
541 * @return bool
542 */
543 public function isSingleton(): bool
544 {
545 return $this->isSingleton;
546 }
547
548 /**
549 * @param string $methodName
550 * @return bool
551 */
552 public function hasMethod(string $methodName): bool
553 {
554 return isset($this->methods[$methodName]);
555 }
556
557 /**
558 * @return array
559 */
560 public function getTags(): array
561 {
562 return $this->tags;
563 }
564
565 /**
566 * @return bool
567 */
568 public function hasInjectProperties(): bool
569 {
570 return count($this->injectProperties) > 0;
571 }
572
573 /**
574 * @return bool
575 */
576 public function hasInjectMethods(): bool
577 {
578 return count($this->injectMethods) > 0;
579 }
580
581 /**
582 * @return array
583 */
584 public function getInjectMethods(): array
585 {
586 $injectMethods = [];
587 foreach ($this->injectMethods as $injectMethodName) {
588 $injectMethods[$injectMethodName] = reset($this->methods[$injectMethodName]['params'])['dependency'];
589 }
590
591 return $injectMethods;
592 }
593
594 /**
595 * @return array
596 */
597 public function getInjectProperties(): array
598 {
599 $injectProperties = [];
600 foreach ($this->injectProperties as $injectPropertyName) {
601 $injectProperties[$injectPropertyName] = $this->properties[$injectPropertyName]['annotations']['dependency'];
602 }
603
604 return $injectProperties;
605 }
606
607 /**
608 * @return array
609 */
610 public function getConstructorArguments(): array
611 {
612 if (!$this->hasConstructor()) {
613 return [];
614 }
615
616 return $this->methods['__construct']['params'];
617 }
618 }