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