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