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