1<?php
2
3declare(strict_types=1);
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
18namespace TYPO3\CMS\Extbase\Reflection;
19
20use Doctrine\Common\Annotations\AnnotationReader;
21use phpDocumentor\Reflection\DocBlock\Tags\Param;
22use phpDocumentor\Reflection\DocBlockFactory;
23use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
24use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
25use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
26use Symfony\Component\PropertyInfo\Type;
27use TYPO3\CMS\Core\SingletonInterface;
28use TYPO3\CMS\Core\Type\BitSet;
29use TYPO3\CMS\Core\Utility\ClassNamingUtility;
30use TYPO3\CMS\Core\Utility\StringUtility;
31use TYPO3\CMS\Extbase\Annotation\IgnoreValidation;
32use TYPO3\CMS\Extbase\Annotation\Inject;
33use TYPO3\CMS\Extbase\Annotation\ORM\Cascade;
34use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
35use TYPO3\CMS\Extbase\Annotation\ORM\Transient;
36use TYPO3\CMS\Extbase\Annotation\Validate;
37use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
38use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
39use TYPO3\CMS\Extbase\Mvc\Controller\ControllerInterface;
40use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchMethodException;
41use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchPropertyException;
42use TYPO3\CMS\Extbase\Reflection\ClassSchema\Method;
43use TYPO3\CMS\Extbase\Reflection\ClassSchema\Property;
44use TYPO3\CMS\Extbase\Reflection\ClassSchema\PropertyCharacteristics;
45use TYPO3\CMS\Extbase\Reflection\DocBlock\Tags\Null_;
46use TYPO3\CMS\Extbase\Validation\Exception\InvalidTypeHintException;
47use TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationConfigurationException;
48use TYPO3\CMS\Extbase\Validation\ValidatorClassNameResolver;
49
50/**
51 * A class schema
52 * @internal only to be used within Extbase, not part of TYPO3 Core API.
53 */
54class ClassSchema
55{
56    private const BIT_CLASS_IS_ENTITY = 1 << 0;
57    private const BIT_CLASS_IS_VALUE_OBJECT = 1 << 1;
58    private const BIT_CLASS_IS_AGGREGATE_ROOT = 1 << 2;
59    private const BIT_CLASS_IS_CONTROLLER = 1 << 3;
60    private const BIT_CLASS_IS_SINGLETON = 1 << 4;
61    private const BIT_CLASS_HAS_CONSTRUCTOR = 1 << 5;
62    private const BIT_CLASS_HAS_INJECT_METHODS = 1 << 6;
63    private const BIT_CLASS_HAS_INJECT_PROPERTIES = 1 << 7;
64
65    /**
66     * @var BitSet
67     */
68    private $bitSet;
69
70    /**
71     * @var array
72     */
73    private static $propertyObjects = [];
74
75    /**
76     * @var array
77     */
78    private static $methodObjects = [];
79
80    /**
81     * Name of the class this schema is referring to
82     *
83     * @var string
84     */
85    protected $className;
86
87    /**
88     * Properties of the class which need to be persisted
89     *
90     * @var array
91     */
92    protected $properties = [];
93
94    /**
95     * @var array
96     */
97    private $methods = [];
98
99    /**
100     * @var array
101     */
102    private $injectMethods = [];
103
104    /**
105     * @var PropertyInfoExtractor
106     */
107    private static $propertyInfoExtractor;
108
109    /**
110     * @var DocBlockFactory
111     */
112    private static $docBlockFactory;
113
114    /**
115     * Constructs this class schema
116     *
117     * @param string $className Name of the class this schema is referring to
118     * @throws InvalidTypeHintException
119     * @throws InvalidValidationConfigurationException
120     * @throws \ReflectionException
121     */
122    public function __construct(string $className)
123    {
124        /** @var class-string $className */
125        $this->className = $className;
126        $this->bitSet = new BitSet();
127
128        $reflectionClass = new \ReflectionClass($className);
129
130        if ($reflectionClass->implementsInterface(SingletonInterface::class)) {
131            $this->bitSet->set(self::BIT_CLASS_IS_SINGLETON);
132        }
133
134        if ($reflectionClass->implementsInterface(ControllerInterface::class)) {
135            $this->bitSet->set(self::BIT_CLASS_IS_CONTROLLER);
136        }
137
138        if ($reflectionClass->isSubclassOf(AbstractEntity::class)) {
139            $this->bitSet->set(self::BIT_CLASS_IS_ENTITY);
140
141            $possibleRepositoryClassName = ClassNamingUtility::translateModelNameToRepositoryName($className);
142            if (class_exists($possibleRepositoryClassName)) {
143                $this->bitSet->set(self::BIT_CLASS_IS_AGGREGATE_ROOT);
144            }
145        }
146
147        if ($reflectionClass->isSubclassOf(AbstractValueObject::class)) {
148            $this->bitSet->set(self::BIT_CLASS_IS_VALUE_OBJECT);
149        }
150
151        if (self::$propertyInfoExtractor === null) {
152            $docBlockFactory = DocBlockFactory::createInstance();
153            $phpDocExtractor = new PhpDocExtractor($docBlockFactory);
154
155            $reflectionExtractor = new ReflectionExtractor();
156
157            self::$propertyInfoExtractor = new PropertyInfoExtractor(
158                [],
159                [$phpDocExtractor, $reflectionExtractor]
160            );
161        }
162
163        if (self::$docBlockFactory === null) {
164            self::$docBlockFactory = DocBlockFactory::createInstance();
165            self::$docBlockFactory->registerTagHandler('author', Null_::class);
166            self::$docBlockFactory->registerTagHandler('covers', Null_::class);
167            self::$docBlockFactory->registerTagHandler('deprecated', Null_::class);
168            self::$docBlockFactory->registerTagHandler('link', Null_::class);
169            self::$docBlockFactory->registerTagHandler('method', Null_::class);
170            self::$docBlockFactory->registerTagHandler('property-read', Null_::class);
171            self::$docBlockFactory->registerTagHandler('property', Null_::class);
172            self::$docBlockFactory->registerTagHandler('property-write', Null_::class);
173            self::$docBlockFactory->registerTagHandler('return', Null_::class);
174            self::$docBlockFactory->registerTagHandler('see', Null_::class);
175            self::$docBlockFactory->registerTagHandler('since', Null_::class);
176            self::$docBlockFactory->registerTagHandler('source', Null_::class);
177            self::$docBlockFactory->registerTagHandler('throw', Null_::class);
178            self::$docBlockFactory->registerTagHandler('throws', Null_::class);
179            self::$docBlockFactory->registerTagHandler('uses', Null_::class);
180            self::$docBlockFactory->registerTagHandler('var', Null_::class);
181            self::$docBlockFactory->registerTagHandler('version', Null_::class);
182        }
183
184        $this->reflectProperties($reflectionClass);
185        $this->reflectMethods($reflectionClass);
186    }
187
188    /**
189     * @param \ReflectionClass $reflectionClass
190     * @throws \Doctrine\Common\Annotations\AnnotationException
191     * @throws \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException
192     */
193    protected function reflectProperties(\ReflectionClass $reflectionClass): void
194    {
195        $annotationReader = new AnnotationReader();
196
197        $classHasInjectProperties = false;
198        $defaultProperties = $reflectionClass->getDefaultProperties();
199
200        foreach ($reflectionClass->getProperties() as $reflectionProperty) {
201            $propertyName = $reflectionProperty->getName();
202            // according to https://www.php.net/manual/en/reflectionclass.getdefaultproperties.php
203            // > This method only works for static properties when used on internal classes. The default
204            // > value of a static class property can not be tracked when using this method on user defined classes.
205            $defaultPropertyValue = $reflectionProperty->isStatic() ? null : $defaultProperties[$propertyName] ?? null;
206
207            $propertyCharacteristicsBit = 0;
208            $propertyCharacteristicsBit += $reflectionProperty->isPrivate() ? PropertyCharacteristics::VISIBILITY_PRIVATE : 0;
209            $propertyCharacteristicsBit += $reflectionProperty->isProtected() ? PropertyCharacteristics::VISIBILITY_PROTECTED : 0;
210            $propertyCharacteristicsBit += $reflectionProperty->isPublic() ? PropertyCharacteristics::VISIBILITY_PUBLIC : 0;
211            $propertyCharacteristicsBit += $reflectionProperty->isStatic() ? PropertyCharacteristics::IS_STATIC : 0;
212
213            $this->properties[$propertyName] = [
214                'c' => null, // cascade
215                'd' => $defaultPropertyValue, // defaultValue
216                'e' => null, // elementType
217                't' => null, // type
218                'v' => [] // validators
219            ];
220
221            $annotations = $annotationReader->getPropertyAnnotations($reflectionProperty);
222
223            /** @var array|Validate[] $validateAnnotations */
224            $validateAnnotations = array_filter($annotations, function ($annotation) {
225                return $annotation instanceof Validate;
226            });
227
228            if (count($validateAnnotations) > 0) {
229                foreach ($validateAnnotations as $validateAnnotation) {
230                    $validatorObjectName = ValidatorClassNameResolver::resolve($validateAnnotation->validator);
231
232                    $this->properties[$propertyName]['v'][] = [
233                        'name' => $validateAnnotation->validator,
234                        'options' => $validateAnnotation->options,
235                        'className' => $validatorObjectName,
236                    ];
237                }
238            }
239
240            if ($annotationReader->getPropertyAnnotation($reflectionProperty, Lazy::class) instanceof Lazy) {
241                $propertyCharacteristicsBit += PropertyCharacteristics::ANNOTATED_LAZY;
242            }
243
244            if ($annotationReader->getPropertyAnnotation($reflectionProperty, Transient::class) instanceof Transient) {
245                $propertyCharacteristicsBit += PropertyCharacteristics::ANNOTATED_TRANSIENT;
246            }
247
248            $isInjectProperty = $propertyName !== 'settings'
249                && ($annotationReader->getPropertyAnnotation($reflectionProperty, Inject::class) instanceof Inject);
250
251            if ($isInjectProperty) {
252                $propertyCharacteristicsBit += PropertyCharacteristics::ANNOTATED_INJECT;
253                $classHasInjectProperties = true;
254            }
255
256            $this->properties[$propertyName]['propertyCharacteristicsBit'] = $propertyCharacteristicsBit;
257
258            /** @var Type[] $types */
259            $types = (array)self::$propertyInfoExtractor->getTypes($this->className, $propertyName, ['reflectionProperty' => $reflectionProperty]);
260            $typesCount = count($types);
261
262            if ($typesCount !== 1) {
263                continue;
264            }
265
266            if (($annotation = $annotationReader->getPropertyAnnotation($reflectionProperty, Cascade::class)) instanceof Cascade) {
267                /** @var Cascade $annotation */
268                $this->properties[$propertyName]['c'] = $annotation->value;
269            }
270
271            /** @var Type $type */
272            $type = current($types);
273
274            if ($type->isCollection()) {
275                $this->properties[$propertyName]['t'] = ltrim($type->getClassName() ?? $type->getBuiltinType(), '\\');
276
277                if (($collectionValueType = $type->getCollectionValueType()) instanceof Type) {
278                    $this->properties[$propertyName]['e'] = ltrim($collectionValueType->getClassName() ?? $type->getBuiltinType(), '\\');
279                }
280            } else {
281                $this->properties[$propertyName]['t'] = $types[0]->getClassName() ?? $types[0]->getBuiltinType();
282            }
283        }
284
285        if ($classHasInjectProperties) {
286            $this->bitSet->set(self::BIT_CLASS_HAS_INJECT_PROPERTIES);
287        }
288    }
289
290    /**
291     * @param \ReflectionClass $reflectionClass
292     * @throws InvalidTypeHintException
293     * @throws InvalidValidationConfigurationException
294     * @throws \Doctrine\Common\Annotations\AnnotationException
295     * @throws \ReflectionException
296     * @throws \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException
297     */
298    protected function reflectMethods(\ReflectionClass $reflectionClass): void
299    {
300        $annotationReader = new AnnotationReader();
301
302        foreach ($reflectionClass->getMethods() as $reflectionMethod) {
303            $methodName = $reflectionMethod->getName();
304
305            $this->methods[$methodName] = [];
306            $this->methods[$methodName]['private']      = $reflectionMethod->isPrivate();
307            $this->methods[$methodName]['protected']    = $reflectionMethod->isProtected();
308            $this->methods[$methodName]['public']       = $reflectionMethod->isPublic();
309            $this->methods[$methodName]['static']       = $reflectionMethod->isStatic();
310            $this->methods[$methodName]['abstract']     = $reflectionMethod->isAbstract();
311            $this->methods[$methodName]['params']       = [];
312            $this->methods[$methodName]['tags']         = [];
313            $this->methods[$methodName]['annotations']  = [];
314            $this->methods[$methodName]['isAction']     = StringUtility::endsWith($methodName, 'Action');
315
316            $argumentValidators = [];
317
318            $annotations = $annotationReader->getMethodAnnotations($reflectionMethod);
319
320            /** @var array|Validate[] $validateAnnotations */
321            $validateAnnotations = array_filter($annotations, function ($annotation) {
322                return $annotation instanceof Validate;
323            });
324
325            if ($this->methods[$methodName]['isAction']
326                && $this->bitSet->get(self::BIT_CLASS_IS_CONTROLLER)
327                && count($validateAnnotations) > 0
328            ) {
329                foreach ($validateAnnotations as $validateAnnotation) {
330                    $validatorName = $validateAnnotation->validator;
331                    $validatorObjectName = ValidatorClassNameResolver::resolve($validatorName);
332
333                    $argumentValidators[$validateAnnotation->param][] = [
334                        'name' => $validatorName,
335                        'options' => $validateAnnotation->options,
336                        'className' => $validatorObjectName,
337                    ];
338                }
339            }
340
341            foreach ($annotations as $annotation) {
342                if ($annotation instanceof IgnoreValidation) {
343                    $this->methods[$methodName]['tags']['ignorevalidation'][] = $annotation->argumentName;
344                }
345            }
346
347            $docComment = $reflectionMethod->getDocComment();
348            $docComment = is_string($docComment) ? $docComment : '';
349
350            foreach ($reflectionMethod->getParameters() as $parameterPosition => $reflectionParameter) {
351                /* @var \ReflectionParameter $reflectionParameter */
352
353                $parameterName = $reflectionParameter->getName();
354
355                $ignoreValidationParameters = array_filter($annotations, function ($annotation) use ($parameterName) {
356                    return $annotation instanceof IgnoreValidation && $annotation->argumentName === $parameterName;
357                });
358
359                $reflectionType = $reflectionParameter->getType();
360
361                $this->methods[$methodName]['params'][$parameterName] = [];
362                $this->methods[$methodName]['params'][$parameterName]['position'] = $parameterPosition; // compat
363                $this->methods[$methodName]['params'][$parameterName]['byReference'] = $reflectionParameter->isPassedByReference(); // compat
364                $this->methods[$methodName]['params'][$parameterName]['array'] = false; // compat
365                $this->methods[$methodName]['params'][$parameterName]['optional'] = $reflectionParameter->isOptional();
366                $this->methods[$methodName]['params'][$parameterName]['allowsNull'] = $reflectionParameter->allowsNull();
367                $this->methods[$methodName]['params'][$parameterName]['class'] = null; // compat
368                $this->methods[$methodName]['params'][$parameterName]['type'] = null;
369                $this->methods[$methodName]['params'][$parameterName]['hasDefaultValue'] = $reflectionParameter->isDefaultValueAvailable();
370                $this->methods[$methodName]['params'][$parameterName]['defaultValue'] = null;
371                $this->methods[$methodName]['params'][$parameterName]['dependency'] = null; // Extbase DI
372                $this->methods[$methodName]['params'][$parameterName]['ignoreValidation'] = count($ignoreValidationParameters) === 1;
373                $this->methods[$methodName]['params'][$parameterName]['validators'] = [];
374
375                if ($reflectionParameter->isDefaultValueAvailable()) {
376                    $this->methods[$methodName]['params'][$parameterName]['defaultValue'] = $reflectionParameter->getDefaultValue();
377                }
378
379                // A ReflectionNamedType means "there is a type specified, and it's not a union type."
380                // (Union types are not handled, currently.)
381                if ($reflectionType instanceof \ReflectionNamedType) {
382                    $this->methods[$methodName]['params'][$parameterName]['allowsNull'] = $reflectionType->allowsNull();
383                    // A built-in type effectively means "not a class".
384                    if ($reflectionType->isBuiltin()) {
385                        $this->methods[$methodName]['params'][$parameterName]['array'] = $reflectionType->getName() === 'array'; // compat
386                        $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($reflectionType->getName(), '\\');
387                    } elseif ($reflectionType->getName() === 'self') {
388                        // In addition, self cannot be resolved by "new \ReflectionClass('self')",
389                        // so treat this as a reference to the current class
390                        $this->methods[$methodName]['params'][$parameterName]['class'] = $reflectionClass->getName();
391                        $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($reflectionClass->getName(), '\\');
392                    } else {
393                        // This is mainly to confirm that the class exists. If it doesn't, a ReflectionException
394                        // will be thrown. It's not the ideal way of doing so, but it maintains the existing API
395                        // so that the exception can get caught and recast to a TYPO3-specific exception.
396                        /** @var class-string<mixed> $classname */
397                        $classname = $reflectionType->getName();
398                        $reflection = new \ReflectionClass($classname);
399                        // There's a single type declaration that is a class.
400                        $this->methods[$methodName]['params'][$parameterName]['class'] = $reflectionType->getName();
401                        $this->methods[$methodName]['params'][$parameterName]['type'] = $reflectionType->getName();
402                    }
403                }
404
405                if ($docComment !== '' && $this->methods[$methodName]['params'][$parameterName]['type'] === null) {
406                    /*
407                     * We create (redundant) instances here in this loop due to the fact that
408                     * we do not want to analyse all doc blocks of all available methods. We
409                     * use this technique only if we couldn't grasp all necessary data via
410                     * reflection.
411                     *
412                     * Also, if we analyze all method doc blocks, we will trigger numerous errors
413                     * due to non PSR-5 compatible tags in the core and in user land code.
414                     *
415                     * Fetching the data type via doc blocks will also be deprecated and removed
416                     * in the near future.
417                     */
418                    $params = self::$docBlockFactory->create($docComment)
419                        ->getTagsByName('param');
420
421                    if (isset($params[$parameterPosition])) {
422                        /** @var Param $param */
423                        $param = $params[$parameterPosition];
424                        $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim((string)$param->getType(), '\\');
425                    }
426                }
427
428                // Extbase DI
429                if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()
430                    && ($reflectionMethod->isConstructor() || $this->hasInjectMethodName($reflectionMethod))
431                ) {
432                    $this->methods[$methodName]['params'][$parameterName]['dependency'] = $reflectionType->getName();
433                }
434
435                // Extbase Validation
436                if (isset($argumentValidators[$parameterName])) {
437                    if ($this->methods[$methodName]['params'][$parameterName]['type'] === null) {
438                        throw new InvalidTypeHintException(
439                            'Missing type information for parameter "$' . $parameterName . '" in ' . $this->className . '->' . $methodName . '(): Either use an @param annotation or use a type hint.',
440                            1515075192
441                        );
442                    }
443
444                    $this->methods[$methodName]['params'][$parameterName]['validators'] = $argumentValidators[$parameterName];
445                    unset($argumentValidators[$parameterName]);
446                }
447            }
448
449            // Extbase Validation
450            foreach ($argumentValidators as $parameterName => $validators) {
451                $validatorNames = array_column($validators, 'name');
452
453                throw new InvalidValidationConfigurationException(
454                    'Invalid validate annotation in ' . $this->className . '->' . $methodName . '(): The following validators have been defined for missing param "$' . $parameterName . '": ' . implode(', ', $validatorNames),
455                    1515073585
456                );
457            }
458
459            // Extbase
460            $this->methods[$methodName]['injectMethod'] = false;
461            if ($this->hasInjectMethodName($reflectionMethod)
462                && count($this->methods[$methodName]['params']) === 1
463                && reset($this->methods[$methodName]['params'])['dependency'] !== null
464            ) {
465                $this->methods[$methodName]['injectMethod'] = true;
466                $this->injectMethods[] = $methodName;
467            }
468        }
469
470        if (isset($this->methods['__construct'])) {
471            $this->bitSet->set(self::BIT_CLASS_HAS_CONSTRUCTOR);
472        }
473
474        if (count($this->injectMethods) > 0) {
475            $this->bitSet->set(self::BIT_CLASS_HAS_INJECT_METHODS);
476        }
477    }
478
479    /**
480     * Returns the class name this schema is referring to
481     *
482     * @return string The class name
483     */
484    public function getClassName(): string
485    {
486        return $this->className;
487    }
488
489    /**
490     * @throws NoSuchPropertyException
491     *
492     * @param string $propertyName
493     * @return Property
494     */
495    public function getProperty(string $propertyName): Property
496    {
497        $properties = $this->buildPropertyObjects();
498
499        if (!isset($properties[$propertyName])) {
500            throw NoSuchPropertyException::create($this->className, $propertyName);
501        }
502
503        return $properties[$propertyName];
504    }
505
506    /**
507     * @return array|Property[]
508     */
509    public function getProperties(): array
510    {
511        return $this->buildPropertyObjects();
512    }
513
514    /**
515     * Whether the class is an aggregate root and therefore accessible through
516     * a repository.
517     *
518     * @return bool TRUE if it is managed
519     */
520    public function isAggregateRoot(): bool
521    {
522        return $this->bitSet->get(self::BIT_CLASS_IS_AGGREGATE_ROOT);
523    }
524
525    /**
526     * If the class schema has a certain property.
527     *
528     * @param string $propertyName Name of the property
529     * @return bool
530     */
531    public function hasProperty(string $propertyName): bool
532    {
533        return array_key_exists($propertyName, $this->properties);
534    }
535
536    /**
537     * @return bool
538     */
539    public function hasConstructor(): bool
540    {
541        return $this->bitSet->get(self::BIT_CLASS_HAS_CONSTRUCTOR);
542    }
543
544    /**
545     * @throws NoSuchMethodException
546     *
547     * @param string $methodName
548     * @return Method
549     */
550    public function getMethod(string $methodName): Method
551    {
552        $methods = $this->buildMethodObjects();
553
554        if (!isset($methods[$methodName])) {
555            throw NoSuchMethodException::create($this->className, $methodName);
556        }
557
558        return $methods[$methodName];
559    }
560
561    /**
562     * @return array|Method[]
563     */
564    public function getMethods(): array
565    {
566        return $this->buildMethodObjects();
567    }
568
569    /**
570     * @param \ReflectionMethod $reflectionMethod
571     * @return bool
572     */
573    protected function hasInjectMethodName(\ReflectionMethod $reflectionMethod): bool
574    {
575        $methodName = $reflectionMethod->getName();
576        if ($methodName === 'injectSettings' || !$reflectionMethod->isPublic()) {
577            return false;
578        }
579
580        if (
581            strpos($reflectionMethod->getName(), 'inject') === 0
582        ) {
583            return true;
584        }
585
586        return false;
587    }
588
589    /**
590     * @return bool
591     * @internal
592     */
593    public function isModel(): bool
594    {
595        return $this->isEntity() || $this->isValueObject();
596    }
597
598    /**
599     * @return bool
600     * @internal
601     */
602    public function isEntity(): bool
603    {
604        return $this->bitSet->get(self::BIT_CLASS_IS_ENTITY);
605    }
606
607    /**
608     * @return bool
609     * @internal
610     */
611    public function isValueObject(): bool
612    {
613        return $this->bitSet->get(self::BIT_CLASS_IS_VALUE_OBJECT);
614    }
615
616    /**
617     * @return bool
618     */
619    public function isSingleton(): bool
620    {
621        return $this->bitSet->get(self::BIT_CLASS_IS_SINGLETON);
622    }
623
624    /**
625     * @param string $methodName
626     * @return bool
627     */
628    public function hasMethod(string $methodName): bool
629    {
630        return isset($this->methods[$methodName]);
631    }
632
633    /**
634     * @return bool
635     */
636    public function hasInjectProperties(): bool
637    {
638        return $this->bitSet->get(self::BIT_CLASS_HAS_INJECT_PROPERTIES);
639    }
640
641    /**
642     * @return bool
643     */
644    public function hasInjectMethods(): bool
645    {
646        return $this->bitSet->get(self::BIT_CLASS_HAS_INJECT_METHODS);
647    }
648
649    /**
650     * @return array|Method[]
651     */
652    public function getInjectMethods(): array
653    {
654        return array_filter($this->buildMethodObjects(), function ($method) {
655            /** @var Method $method */
656            return $method->isInjectMethod();
657        });
658    }
659
660    /**
661     * @return array|Property[]
662     */
663    public function getInjectProperties(): array
664    {
665        return array_filter($this->buildPropertyObjects(), static function ($property) {
666            /** @var Property $property */
667            return $property->isInjectProperty();
668        });
669    }
670
671    /**
672     * @return array|Property[]
673     */
674    private function buildPropertyObjects(): array
675    {
676        if (!isset(static::$propertyObjects[$this->className])) {
677            static::$propertyObjects[$this->className] = [];
678            foreach ($this->properties as $propertyName => $propertyDefinition) {
679                static::$propertyObjects[$this->className][$propertyName] = new Property($propertyName, $propertyDefinition);
680            }
681        }
682
683        return static::$propertyObjects[$this->className];
684    }
685
686    /**
687     * @return array|Method[]
688     */
689    private function buildMethodObjects(): array
690    {
691        if (!isset(static::$methodObjects[$this->className])) {
692            static::$methodObjects[$this->className] = [];
693            foreach ($this->methods as $methodName => $methodDefinition) {
694                static::$methodObjects[$this->className][$methodName] = new Method($methodName, $methodDefinition, $this->className);
695            }
696        }
697
698        return static::$methodObjects[$this->className];
699    }
700}
701