[TASK] Deprecate usage of @inject with non-public properties
[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 TYPO3\CMS\Core\SingletonInterface;
18 use TYPO3\CMS\Core\Utility\ClassNamingUtility;
19 use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
20 use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
21 use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
22
23 /**
24 * A class schema
25 *
26 * @internal
27 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 or later
28 */
29 class ClassSchema
30 {
31 /**
32 * Available model types
33 */
34 const MODELTYPE_ENTITY = 1;
35 const MODELTYPE_VALUEOBJECT = 2;
36
37 /**
38 * Name of the class this schema is referring to
39 *
40 * @var string
41 */
42 protected $className;
43
44 /**
45 * Model type of the class this schema is referring to
46 *
47 * @var int
48 */
49 protected $modelType = self::MODELTYPE_ENTITY;
50
51 /**
52 * Whether a repository exists for the class this schema is referring to
53 *
54 * @var bool
55 */
56 protected $aggregateRoot = false;
57
58 /**
59 * The name of the property holding the uuid of an entity, if any.
60 *
61 * @var string
62 */
63 protected $uuidPropertyName;
64
65 /**
66 * Properties of the class which need to be persisted
67 *
68 * @var array
69 */
70 protected $properties = [];
71
72 /**
73 * The properties forming the identity of an object
74 *
75 * @var array
76 */
77 protected $identityProperties = [];
78
79 /**
80 * Indicates if the class is a singleton or not.
81 *
82 * @var bool
83 */
84 private $isSingleton;
85
86 /**
87 * @var array
88 */
89 private $methods;
90
91 /**
92 * @var array
93 */
94 protected static $ignoredTags = ['package', 'subpackage', 'license', 'copyright', 'author', 'version', 'const'];
95
96 /**
97 * @var array
98 */
99 private $tags = [];
100
101 /**
102 * @var array
103 */
104 private $injectProperties = [];
105
106 /**
107 * @var array
108 */
109 private $injectMethods = [];
110
111 /**
112 * Constructs this class schema
113 *
114 * @param string $className Name of the class this schema is referring to
115 * @throws \TYPO3\CMS\Extbase\Reflection\Exception\UnknownClassException
116 * @throws \ReflectionException
117 */
118 public function __construct($className)
119 {
120 $this->className = $className;
121
122 $reflectionClass = new \ReflectionClass($className);
123
124 $this->isSingleton = $reflectionClass->implementsInterface(SingletonInterface::class);
125
126 if ($reflectionClass->isSubclassOf(AbstractEntity::class)) {
127 $this->modelType = static::MODELTYPE_ENTITY;
128
129 $possibleRepositoryClassName = ClassNamingUtility::translateModelNameToRepositoryName($className);
130 if (class_exists($possibleRepositoryClassName)) {
131 $this->setAggregateRoot(true);
132 }
133 }
134
135 if ($reflectionClass->isSubclassOf(AbstractValueObject::class)) {
136 $this->modelType = static::MODELTYPE_VALUEOBJECT;
137 }
138
139 $docCommentParser = new DocCommentParser();
140 $docCommentParser->parseDocComment($reflectionClass->getDocComment());
141 foreach ($docCommentParser->getTagsValues() as $tag => $values) {
142 if (in_array($tag, static::$ignoredTags, true)) {
143 continue;
144 }
145
146 $this->tags[$tag] = $values;
147 }
148
149 $this->reflectProperties($reflectionClass);
150 $this->reflectMethods($reflectionClass);
151 }
152
153 /**
154 * @param \ReflectionClass $reflectionClass
155 */
156 protected function reflectProperties(\ReflectionClass $reflectionClass)
157 {
158 foreach ($reflectionClass->getProperties() as $reflectionProperty) {
159 $propertyName = $reflectionProperty->getName();
160
161 $this->properties[$propertyName] = [
162 'default' => $reflectionProperty->isDefault(),
163 'private' => $reflectionProperty->isPrivate(),
164 'protected' => $reflectionProperty->isProtected(),
165 'public' => $reflectionProperty->isPublic(),
166 'static' => $reflectionProperty->isStatic(),
167 'type' => null, // Extbase
168 'elementType' => null, // Extbase
169 'annotations' => [],
170 'tags' => []
171 ];
172
173 $docCommentParser = new DocCommentParser();
174 $docCommentParser->parseDocComment($reflectionProperty->getDocComment());
175 foreach ($docCommentParser->getTagsValues() as $tag => $values) {
176 if (in_array($tag, static::$ignoredTags, true)) {
177 continue;
178 }
179
180 $this->properties[$propertyName]['tags'][$tag] = $values;
181 }
182
183 $this->properties[$propertyName]['annotations']['inject'] = false;
184 $this->properties[$propertyName]['annotations']['lazy'] = $docCommentParser->isTaggedWith('lazy');
185 $this->properties[$propertyName]['annotations']['transient'] = $docCommentParser->isTaggedWith('transient');
186 $this->properties[$propertyName]['annotations']['type'] = null;
187 $this->properties[$propertyName]['annotations']['cascade'] = null;
188 $this->properties[$propertyName]['annotations']['dependency'] = null;
189
190 if ($propertyName !== 'settings' && $docCommentParser->isTaggedWith('inject')) {
191 try {
192 $varValues = $docCommentParser->getTagValues('var');
193 $this->properties[$propertyName]['annotations']['inject'] = true;
194 $this->properties[$propertyName]['annotations']['type'] = ltrim($varValues[0], '\\');
195 $this->properties[$propertyName]['annotations']['dependency'] = ltrim($varValues[0], '\\');
196
197 if (!$reflectionProperty->isPublic()) {
198 trigger_error(
199 'Using @inject with non-public properties is deprecated since TYPO3 v9.0 and will stop working in TYPO3 v10.0.',
200 E_USER_DEPRECATED
201 );
202 }
203
204 $this->injectProperties[] = $propertyName;
205 } catch (\Exception $e) {
206 }
207 }
208
209 if ($docCommentParser->isTaggedWith('var') && !$docCommentParser->isTaggedWith('transient')) {
210 try {
211 $cascadeAnnotationValues = $docCommentParser->getTagValues('cascade');
212 $this->properties[$propertyName]['annotations']['cascade'] = $cascadeAnnotationValues[0];
213 } catch (\Exception $e) {
214 }
215
216 try {
217 $type = TypeHandlingUtility::parseType(implode(' ', $docCommentParser->getTagValues('var')));
218 } catch (\Exception $e) {
219 $type = [
220 'type' => null,
221 'elementType' => null
222 ];
223 }
224
225 $this->properties[$propertyName]['type'] = $type['type'] ? ltrim($type['type'], '\\') : null;
226 $this->properties[$propertyName]['elementType'] = $type['elementType'] ? ltrim($type['elementType'], '\\') : null;
227 }
228
229 if ($docCommentParser->isTaggedWith('uuid')) {
230 $this->setUuidPropertyName($propertyName);
231 }
232
233 if ($docCommentParser->isTaggedWith('identity')) {
234 $this->markAsIdentityProperty($propertyName);
235 }
236 }
237 }
238
239 /**
240 * @param \ReflectionClass $reflectionClass
241 */
242 protected function reflectMethods(\ReflectionClass $reflectionClass)
243 {
244 foreach ($reflectionClass->getMethods() as $reflectionMethod) {
245 $methodName = $reflectionMethod->getName();
246
247 $this->methods[$methodName] = [];
248 $this->methods[$methodName]['private'] = $reflectionMethod->isPrivate();
249 $this->methods[$methodName]['protected'] = $reflectionMethod->isProtected();
250 $this->methods[$methodName]['public'] = $reflectionMethod->isPublic();
251 $this->methods[$methodName]['static'] = $reflectionMethod->isStatic();
252 $this->methods[$methodName]['abstract'] = $reflectionMethod->isAbstract();
253 $this->methods[$methodName]['params'] = [];
254 $this->methods[$methodName]['tags'] = [];
255
256 $docCommentParser = new DocCommentParser();
257 $docCommentParser->parseDocComment($reflectionMethod->getDocComment());
258 foreach ($docCommentParser->getTagsValues() as $tag => $values) {
259 if (in_array($tag, static::$ignoredTags, true)) {
260 continue;
261 }
262
263 $this->methods[$methodName]['tags'][$tag] = $values;
264 }
265
266 $this->methods[$methodName]['description'] = $docCommentParser->getDescription();
267
268 foreach ($reflectionMethod->getParameters() as $parameterPosition => $reflectionParameter) {
269 /* @var $reflectionParameter \ReflectionParameter */
270
271 $parameterName = $reflectionParameter->getName();
272
273 $this->methods[$methodName]['params'][$parameterName] = [];
274 $this->methods[$methodName]['params'][$parameterName]['position'] = $parameterPosition; // compat
275 $this->methods[$methodName]['params'][$parameterName]['byReference'] = $reflectionParameter->isPassedByReference(); // compat
276 $this->methods[$methodName]['params'][$parameterName]['array'] = $reflectionParameter->isArray(); // compat
277 $this->methods[$methodName]['params'][$parameterName]['optional'] = $reflectionParameter->isOptional();
278 $this->methods[$methodName]['params'][$parameterName]['allowsNull'] = $reflectionParameter->allowsNull(); // compat
279 $this->methods[$methodName]['params'][$parameterName]['class'] = null; // compat
280 $this->methods[$methodName]['params'][$parameterName]['type'] = null;
281 $this->methods[$methodName]['params'][$parameterName]['nullable'] = $reflectionParameter->allowsNull();
282 $this->methods[$methodName]['params'][$parameterName]['default'] = null;
283 $this->methods[$methodName]['params'][$parameterName]['hasDefaultValue'] = $reflectionParameter->isDefaultValueAvailable();
284 $this->methods[$methodName]['params'][$parameterName]['defaultValue'] = null; // compat
285 $this->methods[$methodName]['params'][$parameterName]['dependency'] = null; // Extbase DI
286
287 if ($reflectionParameter->isDefaultValueAvailable()) {
288 $this->methods[$methodName]['params'][$parameterName]['default'] = $reflectionParameter->getDefaultValue();
289 $this->methods[$methodName]['params'][$parameterName]['defaultValue'] = $reflectionParameter->getDefaultValue(); // compat
290 }
291
292 if (($reflectionType = $reflectionParameter->getType()) instanceof \ReflectionType) {
293 $this->methods[$methodName]['params'][$parameterName]['type'] = (string)$reflectionType;
294 $this->methods[$methodName]['params'][$parameterName]['nullable'] = $reflectionType->allowsNull();
295 }
296
297 if (($parameterClass = $reflectionParameter->getClass()) instanceof \ReflectionClass) {
298 $this->methods[$methodName]['params'][$parameterName]['class'] = $parameterClass->getName();
299 $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($parameterClass->getName(), '\\');
300 } else {
301 $methodTagsAndValues = $this->methods[$methodName]['tags'];
302 if (isset($methodTagsAndValues['param'], $methodTagsAndValues['param'][$parameterPosition])) {
303 $explodedParameters = explode(' ', $methodTagsAndValues['param'][$parameterPosition]);
304 if (count($explodedParameters) >= 2) {
305 if (TypeHandlingUtility::isSimpleType($explodedParameters[0])) {
306 // ensure that short names of simple types are resolved correctly to the long form
307 // this is important for all kinds of type checks later on
308 $typeInfo = TypeHandlingUtility::parseType($explodedParameters[0]);
309
310 $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($typeInfo['type'], '\\');
311 } else {
312 $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($explodedParameters[0], '\\');
313 }
314 }
315 }
316 }
317
318 // Extbase DI
319 if ($reflectionParameter->getClass() instanceof \ReflectionClass
320 && ($reflectionMethod->isConstructor() || $this->hasInjectMethodName($reflectionMethod))
321 ) {
322 $this->methods[$methodName]['params'][$parameterName]['dependency'] = $reflectionParameter->getClass()->getName();
323 }
324 }
325
326 // Extbase
327 $this->methods[$methodName]['injectMethod'] = false;
328 if ($this->hasInjectMethodName($reflectionMethod)
329 && count($this->methods[$methodName]['params']) === 1
330 && reset($this->methods[$methodName]['params'])['dependency'] !== null
331 ) {
332 $this->methods[$methodName]['injectMethod'] = true;
333 $this->injectMethods[] = $methodName;
334 }
335 }
336 }
337
338 /**
339 * Returns the class name this schema is referring to
340 *
341 * @return string The class name
342 */
343 public function getClassName(): string
344 {
345 return $this->className;
346 }
347
348 /**
349 * Adds (defines) a specific property and its type.
350 *
351 * @param string $name Name of the property
352 * @param string $type Type of the property
353 * @param bool $lazy Whether the property should be lazy-loaded when reconstituting
354 * @param string $cascade Strategy to cascade the object graph.
355 * @deprecated
356 */
357 public function addProperty($name, $type, $lazy = false, $cascade = '')
358 {
359 trigger_error(
360 'This method will be removed in TYPO3 v10.0, properties will be automatically added on ClassSchema construction.',
361 E_USER_DEPRECATED
362 );
363 $type = TypeHandlingUtility::parseType($type);
364 $this->properties[$name] = [
365 'type' => $type['type'],
366 'elementType' => $type['elementType'],
367 'lazy' => $lazy,
368 'cascade' => $cascade
369 ];
370 }
371
372 /**
373 * Returns the given property defined in this schema. Check with
374 * hasProperty($propertyName) before!
375 *
376 * @param string $propertyName
377 * @return array
378 */
379 public function getProperty($propertyName)
380 {
381 return is_array($this->properties[$propertyName]) ? $this->properties[$propertyName] : [];
382 }
383
384 /**
385 * Returns all properties defined in this schema
386 *
387 * @return array
388 */
389 public function getProperties()
390 {
391 return $this->properties;
392 }
393
394 /**
395 * Sets the model type of the class this schema is referring to.
396 *
397 * @param int $modelType The model type, one of the MODELTYPE_* constants.
398 * @throws \InvalidArgumentException
399 * @deprecated
400 */
401 public function setModelType($modelType)
402 {
403 trigger_error(
404 'This method will be removed in TYPO3 v10.0, modelType will be automatically set on ClassSchema construction.',
405 E_USER_DEPRECATED
406 );
407 if ($modelType < self::MODELTYPE_ENTITY || $modelType > self::MODELTYPE_VALUEOBJECT) {
408 throw new \InvalidArgumentException('"' . $modelType . '" is an invalid model type.', 1212519195);
409 }
410 $this->modelType = $modelType;
411 }
412
413 /**
414 * Returns the model type of the class this schema is referring to.
415 *
416 * @return int The model type, one of the MODELTYPE_* constants.
417 * @deprecated
418 */
419 public function getModelType()
420 {
421 trigger_error(
422 'This method will be removed in TYPO3 v10.0.',
423 E_USER_DEPRECATED
424 );
425 return $this->modelType;
426 }
427
428 /**
429 * Marks the class if it is root of an aggregate and therefore accessible
430 * through a repository - or not.
431 *
432 * @param bool $isRoot TRUE if it is the root of an aggregate
433 */
434 public function setAggregateRoot($isRoot)
435 {
436 $this->aggregateRoot = $isRoot;
437 }
438
439 /**
440 * Whether the class is an aggregate root and therefore accessible through
441 * a repository.
442 *
443 * @return bool TRUE if it is managed
444 */
445 public function isAggregateRoot(): bool
446 {
447 return $this->aggregateRoot;
448 }
449
450 /**
451 * If the class schema has a certain property.
452 *
453 * @param string $propertyName Name of the property
454 * @return bool
455 */
456 public function hasProperty($propertyName): bool
457 {
458 return array_key_exists($propertyName, $this->properties);
459 }
460
461 /**
462 * Sets the property marked as uuid of an object with @uuid
463 *
464 * @param string $propertyName
465 * @throws \InvalidArgumentException
466 * @deprecated
467 */
468 public function setUuidPropertyName($propertyName)
469 {
470 trigger_error(
471 'Tagging properties with @uuid is deprecated and will be removed in TYPO3 v10.0.',
472 E_USER_DEPRECATED
473 );
474 if (!array_key_exists($propertyName, $this->properties)) {
475 throw new \InvalidArgumentException('Property "' . $propertyName . '" must be added to the class schema before it can be marked as UUID property.', 1233863842);
476 }
477 $this->uuidPropertyName = $propertyName;
478 }
479
480 /**
481 * Gets the name of the property marked as uuid of an object
482 *
483 * @return string
484 * @deprecated
485 */
486 public function getUuidPropertyName()
487 {
488 trigger_error(
489 'Tagging properties with @uuid is deprecated and will be removed in TYPO3 v10.0.',
490 E_USER_DEPRECATED
491 );
492 return $this->uuidPropertyName;
493 }
494
495 /**
496 * Marks the given property as one of properties forming the identity
497 * of an object. The property must already be registered in the class
498 * schema.
499 *
500 * @param string $propertyName
501 * @throws \InvalidArgumentException
502 * @deprecated
503 */
504 public function markAsIdentityProperty($propertyName)
505 {
506 trigger_error(
507 'Tagging properties with @identity is deprecated and will be removed in TYPO3 v10.0.',
508 E_USER_DEPRECATED
509 );
510 if (!array_key_exists($propertyName, $this->properties)) {
511 throw new \InvalidArgumentException('Property "' . $propertyName . '" must be added to the class schema before it can be marked as identity property.', 1233775407);
512 }
513 if ($this->properties[$propertyName]['annotations']['lazy'] === true) {
514 throw new \InvalidArgumentException('Property "' . $propertyName . '" must not be makred for lazy loading to be marked as identity property.', 1239896904);
515 }
516 $this->identityProperties[$propertyName] = $this->properties[$propertyName]['type'];
517 }
518
519 /**
520 * Gets the properties (names and types) forming the identity of an object.
521 *
522 * @return array
523 * @see markAsIdentityProperty()
524 * @deprecated
525 */
526 public function getIdentityProperties()
527 {
528 trigger_error(
529 'Tagging properties with @identity is deprecated and will be removed in TYPO3 v10.0.',
530 E_USER_DEPRECATED
531 );
532 return $this->identityProperties;
533 }
534
535 /**
536 * @return bool
537 */
538 public function hasConstructor(): bool
539 {
540 return isset($this->methods['__construct']);
541 }
542
543 /**
544 * @param string $name
545 * @return array
546 */
547 public function getMethod(string $name): array
548 {
549 return $this->methods[$name] ?? [];
550 }
551
552 /**
553 * @return array
554 */
555 public function getMethods(): array
556 {
557 return $this->methods;
558 }
559
560 /**
561 * @param \ReflectionMethod $reflectionMethod
562 * @return bool
563 */
564 protected function hasInjectMethodName(\ReflectionMethod $reflectionMethod): bool
565 {
566 $methodName = $reflectionMethod->getName();
567 if ($methodName === 'injectSettings' || !$reflectionMethod->isPublic()) {
568 return false;
569 }
570
571 if (
572 strpos($reflectionMethod->getName(), 'inject') === 0
573 ) {
574 return true;
575 }
576
577 return false;
578 }
579
580 /**
581 * @return bool
582 * @internal
583 */
584 public function isModel(): bool
585 {
586 return $this->isEntity() || $this->isValueObject();
587 }
588
589 /**
590 * @return bool
591 * @internal
592 */
593 public function isEntity(): bool
594 {
595 return $this->modelType === static::MODELTYPE_ENTITY;
596 }
597
598 /**
599 * @return bool
600 * @internal
601 */
602 public function isValueObject(): bool
603 {
604 return $this->modelType === static::MODELTYPE_VALUEOBJECT;
605 }
606
607 /**
608 * @return bool
609 */
610 public function isSingleton(): bool
611 {
612 return $this->isSingleton;
613 }
614
615 /**
616 * @param string $methodName
617 * @return bool
618 */
619 public function hasMethod(string $methodName): bool
620 {
621 return isset($this->methods[$methodName]);
622 }
623
624 /**
625 * @return array
626 */
627 public function getTags(): array
628 {
629 return $this->tags;
630 }
631
632 /**
633 * @return bool
634 */
635 public function hasInjectProperties(): bool
636 {
637 return count($this->injectProperties) > 0;
638 }
639
640 /**
641 * @return bool
642 */
643 public function hasInjectMethods(): bool
644 {
645 return count($this->injectMethods) > 0;
646 }
647
648 /**
649 * @return array
650 */
651 public function getInjectMethods(): array
652 {
653 $injectMethods = [];
654 foreach ($this->injectMethods as $injectMethodName) {
655 $injectMethods[$injectMethodName] = reset($this->methods[$injectMethodName]['params'])['dependency'];
656 }
657
658 return $injectMethods;
659 }
660
661 /**
662 * @return array
663 */
664 public function getInjectProperties(): array
665 {
666 $injectProperties = [];
667 foreach ($this->injectProperties as $injectPropertyName) {
668 $injectProperties[$injectPropertyName] = $this->properties[$injectPropertyName]['annotations']['dependency'];
669 }
670
671 return $injectProperties;
672 }
673
674 /**
675 * @return array
676 */
677 public function getConstructorArguments(): array
678 {
679 if (!$this->hasConstructor()) {
680 return [];
681 }
682
683 return $this->methods['__construct']['params'];
684 }
685 }