*/ private array $properties = []; private array $methods = []; private static ?PropertyInfoExtractor $propertyInfoExtractor = null; /** * Constructs this class schema * * @param class-string $className Name of the class this schema is referring to * @throws InvalidTypeHintException * @throws InvalidValidationConfigurationException * @throws \ReflectionException */ public function __construct(private readonly string $className) { $this->bitSet = new BitSet(); $reflectionClass = new \ReflectionClass($className); if ($reflectionClass->implementsInterface(ControllerInterface::class)) { $this->bitSet->set(self::BIT_CLASS_IS_CONTROLLER); } if (self::$propertyInfoExtractor === null) { $docBlockFactory = DocBlockFactory::createInstance(); $phpDocExtractor = new PhpDocExtractor($docBlockFactory); $reflectionExtractor = new ReflectionExtractor(); self::$propertyInfoExtractor = new PropertyInfoExtractor( [], [$phpDocExtractor, $reflectionExtractor] ); } $this->reflectProperties($reflectionClass); $this->reflectMethods($reflectionClass); } /** * @throws \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException */ protected function reflectProperties(\ReflectionClass $reflectionClass): void { foreach ($reflectionClass->getProperties() as $reflectionProperty) { if ($reflectionProperty->isStatic()) { continue; } $propertyName = $reflectionProperty->getName(); $propertyCharacteristicsBit = 0; $propertyCharacteristicsBit += $reflectionProperty->isPrivate() ? PropertyCharacteristics::VISIBILITY_PRIVATE : 0; $propertyCharacteristicsBit += $reflectionProperty->isProtected() ? PropertyCharacteristics::VISIBILITY_PROTECTED : 0; $propertyCharacteristicsBit += $reflectionProperty->isPublic() ? PropertyCharacteristics::VISIBILITY_PUBLIC : 0; $this->properties[$propertyName] = [ 'c' => null, // cascade 'f' => null, // file upload 't' => null, // type 'v' => [], // validators ]; $validateAttributes = []; $fileUploadAttributes = []; foreach ($reflectionProperty->getAttributes() as $attribute) { match ($attribute->getName()) { Attribute\Validate::class => $validateAttributes[] = $attribute, Attribute\FileUpload::class => $fileUploadAttributes[] = $attribute, Attribute\ORM\Lazy::class => $propertyCharacteristicsBit += PropertyCharacteristics::ANNOTATED_LAZY, Attribute\ORM\Transient::class => $propertyCharacteristicsBit += PropertyCharacteristics::ANNOTATED_TRANSIENT, Attribute\ORM\Cascade::class => $this->properties[$propertyName]['c'] = $attribute->newInstance()->value, default => '' // non-extbase attributes }; if (is_a($attribute->getName(), Constraint::class, true)) { $validateAttributes[] = $attribute; } } foreach ($validateAttributes as $attribute) { $validator = $attribute->newInstance(); if ($validator instanceof Constraint) { $property = [ 'constraint' => $validator, 'className' => $validator::class, ]; } else { $property = [ 'name' => $validator->validator, 'options' => $validator->options, 'className' => ValidatorClassNameResolver::resolve($validator->validator), ]; } $this->properties[$propertyName]['v'][] = $property; } foreach ($fileUploadAttributes as $attribute) { $fileUpload = $attribute->newInstance(); $this->properties[$propertyName]['f'] = [ 'validation' => $fileUpload->validation, 'uploadFolder' => $fileUpload->uploadFolder, 'addRandomSuffix' => $fileUpload->addRandomSuffix, 'duplicationBehavior' => $fileUpload->duplicationBehavior, 'createUploadFolderIfNotExist' => $fileUpload->createUploadFolderIfNotExist, ]; } $this->properties[$propertyName]['propertyCharacteristicsBit'] = $propertyCharacteristicsBit; $type = self::$propertyInfoExtractor->getType($this->className, $propertyName, ['reflectionProperty' => $reflectionProperty]); if ($type !== null) { $this->properties[$propertyName]['t'] = $type; } } } /** * @throws InvalidTypeHintException * @throws InvalidValidationConfigurationException * @throws \ReflectionException * @throws \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException */ protected function reflectMethods(\ReflectionClass $reflectionClass): void { foreach ($reflectionClass->getMethods() as $reflectionMethod) { if ($reflectionMethod->isStatic()) { continue; } $methodName = $reflectionMethod->getName(); $this->methods[$methodName] = []; $this->methods[$methodName]['private'] = $reflectionMethod->isPrivate(); $this->methods[$methodName]['protected'] = $reflectionMethod->isProtected(); $this->methods[$methodName]['public'] = $reflectionMethod->isPublic(); $this->methods[$methodName]['params'] = []; $isAction = $this->bitSet->get(self::BIT_CLASS_IS_CONTROLLER) && str_ends_with($methodName, 'Action'); /** @var array> $validateAttributes */ $validateAttributes = []; /** @var array> $validateAttributes */ $ignoreValidationAttributes = []; foreach ($reflectionMethod->getParameters() as $reflectionParameter) { $parameterName = $reflectionParameter->getName(); $parameterAttributes = $reflectionParameter->getAttributes(); $validateAttributes[$parameterName] ??= []; $ignoreValidationAttributes[$parameterName] ??= []; if ($isAction) { foreach ($parameterAttributes as $parameterAttribute) { match ($parameterAttribute->getName()) { Attribute\Validate::class => $validateAttributes[$parameterName][] = $parameterAttribute->newInstance(), Attribute\IgnoreValidation::class => $ignoreValidationAttributes[$parameterName][] = $parameterAttribute->newInstance(), default => '' // non-extbase attributes }; } } $reflectionType = $reflectionParameter->getType(); $this->methods[$methodName]['params'][$parameterName] = []; $this->methods[$methodName]['params'][$parameterName]['array'] = false; // compat $this->methods[$methodName]['params'][$parameterName]['optional'] = $reflectionParameter->isOptional(); $this->methods[$methodName]['params'][$parameterName]['allowsNull'] = $reflectionParameter->allowsNull(); $this->methods[$methodName]['params'][$parameterName]['type'] = null; $this->methods[$methodName]['params'][$parameterName]['hasDefaultValue'] = $reflectionParameter->isDefaultValueAvailable(); $this->methods[$methodName]['params'][$parameterName]['defaultValue'] = null; $this->methods[$methodName]['params'][$parameterName]['ignoreValidation'] = $ignoreValidationAttributes[$parameterName] !== []; $this->methods[$methodName]['params'][$parameterName]['validators'] = []; if ($reflectionParameter->isDefaultValueAvailable()) { $this->methods[$methodName]['params'][$parameterName]['defaultValue'] = $reflectionParameter->getDefaultValue(); } // A ReflectionNamedType means "there is a type specified, and it's not a union type." // (Union types are not handled, currently.) if ($reflectionType instanceof \ReflectionNamedType) { $this->methods[$methodName]['params'][$parameterName]['allowsNull'] = $reflectionType->allowsNull(); // A built-in type effectively means "not a class". if ($reflectionType->isBuiltin()) { $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($reflectionType->getName(), '\\'); } elseif ($reflectionType->getName() === 'self') { // In addition, self cannot be resolved by "new \ReflectionClass('self')", // so treat this as a reference to the current class $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($reflectionClass->getName(), '\\'); } else { // This is mainly to confirm that the class exists. If it doesn't, a ReflectionException // will be thrown. It's not the ideal way of doing so, but it maintains the existing API // so that the exception can get caught and recast to a TYPO3-specific exception. /** @var class-string $classname */ $classname = $reflectionType->getName(); // Test if the class can be reflected /** @noinspection PhpUnusedLocalVariableInspection */ $reflection = new \ReflectionClass($classname); // There's a single type declaration that is a class. $this->methods[$methodName]['params'][$parameterName]['type'] = $reflectionType->getName(); } } // Extbase Validation if ($validateAttributes[$parameterName] !== []) { if ($this->methods[$methodName]['params'][$parameterName]['type'] === null) { throw new InvalidTypeHintException( 'Missing type information for parameter "$' . $parameterName . '" in ' . $this->className . '->' . $methodName . '(): Use a type hint.', 1515075192 ); } $this->methods[$methodName]['params'][$parameterName]['validators'] = array_map( static fn(Attribute\Validate $validator) => [ 'name' => $validator->validator, 'options' => $validator->options, 'className' => ValidatorClassNameResolver::resolve($validator->validator), ], $validateAttributes[$parameterName], ); unset($validateAttributes[$parameterName]); } } // Extbase Validation foreach ($validateAttributes as $parameterName => $validators) { if ($validators !== []) { $validatorNames = array_map( static fn(Attribute\Validate $validate) => $validate->validator, $validators, ); throw new InvalidValidationConfigurationException( 'Invalid #[Validate] attribute in ' . $this->className . '->' . $methodName . '(): The following validators have been defined for missing param "$' . $parameterName . '": ' . implode(', ', $validatorNames), 1515073585 ); } } } } /** * @throws NoSuchPropertyException */ public function getProperty(string $propertyName): Property { $properties = $this->buildPropertyObjects(); if (!isset($properties[$propertyName])) { throw NoSuchPropertyException::create($this->className, $propertyName); } return $properties[$propertyName]; } /** * @return array|Property[] */ public function getProperties(): array { return $this->buildPropertyObjects(); } /** * Returns all properties that do not start with an underscore like $_localizedUid * * @return Property[] * @internal */ public function getDomainObjectProperties(): array { return array_filter( $this->getProperties(), static fn(Property $property): bool => !str_starts_with($property->getName(), '_') ); } /** * If the class schema has a certain property. * * @param string $propertyName Name of the property */ public function hasProperty(string $propertyName): bool { return array_key_exists($propertyName, $this->properties); } /** * @throws NoSuchMethodException */ public function getMethod(string $methodName): Method { $methods = $this->buildMethodObjects(); if (!isset($methods[$methodName])) { throw NoSuchMethodException::create($this->className, $methodName); } return $methods[$methodName]; } /** * @return array|Method[] */ public function getMethods(): array { return $this->buildMethodObjects(); } public function hasMethod(string $methodName): bool { return isset($this->methods[$methodName]); } /** * @return array|Property[] */ private function buildPropertyObjects(): array { if (!isset(self::$propertyObjects[$this->className])) { self::$propertyObjects[$this->className] = []; foreach ($this->properties as $propertyName => $propertyDefinition) { self::$propertyObjects[$this->className][$propertyName] = new Property($propertyName, $propertyDefinition); } } return self::$propertyObjects[$this->className]; } /** * @return array|Method[] */ private function buildMethodObjects(): array { if (!isset(self::$methodObjects[$this->className])) { self::$methodObjects[$this->className] = []; foreach ($this->methods as $methodName => $methodDefinition) { self::$methodObjects[$this->className][$methodName] = new Method($methodName, $methodDefinition, $this->className); } } return self::$methodObjects[$this->className]; } }