className = $className; $reflectionClass = new \ReflectionClass($className); $this->isSingleton = $reflectionClass->implementsInterface(SingletonInterface::class); $this->isController = $reflectionClass->implementsInterface(ControllerInterface::class); if ($reflectionClass->isSubclassOf(AbstractEntity::class)) { $this->modelType = static::MODELTYPE_ENTITY; $possibleRepositoryClassName = ClassNamingUtility::translateModelNameToRepositoryName($className); if (class_exists($possibleRepositoryClassName)) { $this->setAggregateRoot(true); } } if ($reflectionClass->isSubclassOf(AbstractValueObject::class)) { $this->modelType = static::MODELTYPE_VALUEOBJECT; } $docCommentParser = new DocCommentParser(true); $docCommentParser->parseDocComment($reflectionClass->getDocComment()); $this->tags = $docCommentParser->getTagsValues(); $this->reflectProperties($reflectionClass); $this->reflectMethods($reflectionClass); } /** * @param \ReflectionClass $reflectionClass */ protected function reflectProperties(\ReflectionClass $reflectionClass) { $annotationReader = new AnnotationReader(); foreach ($reflectionClass->getProperties() as $reflectionProperty) { $propertyName = $reflectionProperty->getName(); $this->properties[$propertyName] = [ 'default' => $reflectionProperty->isDefault(), 'private' => $reflectionProperty->isPrivate(), 'protected' => $reflectionProperty->isProtected(), 'public' => $reflectionProperty->isPublic(), 'static' => $reflectionProperty->isStatic(), 'type' => null, // Extbase 'elementType' => null, // Extbase 'annotations' => [], 'tags' => [], 'validators' => [] ]; $docCommentParser = new DocCommentParser(true); $docCommentParser->parseDocComment($reflectionProperty->getDocComment()); foreach ($docCommentParser->getTagsValues() as $tag => $values) { $this->properties[$propertyName]['tags'][strtolower($tag)] = $values; } $this->properties[$propertyName]['annotations']['inject'] = false; $this->properties[$propertyName]['annotations']['lazy'] = false; $this->properties[$propertyName]['annotations']['transient'] = false; $this->properties[$propertyName]['annotations']['type'] = null; $this->properties[$propertyName]['annotations']['cascade'] = null; $this->properties[$propertyName]['annotations']['dependency'] = null; $annotations = $annotationReader->getPropertyAnnotations($reflectionProperty); /** @var array|Validate[] $validateAnnotations */ $validateAnnotations = array_filter($annotations, function ($annotation) { return $annotation instanceof Validate; }); if (count($validateAnnotations) > 0) { $validatorResolver = GeneralUtility::makeInstance(ValidatorResolver::class); foreach ($validateAnnotations as $validateAnnotation) { $validatorObjectName = $validatorResolver->resolveValidatorObjectName($validateAnnotation->validator); $this->properties[$propertyName]['validators'][] = [ 'name' => $validateAnnotation->validator, 'options' => $validateAnnotation->options, 'className' => $validatorObjectName, ]; } } if ($docCommentParser->isTaggedWith('validate')) { trigger_error( sprintf( 'Property %s::%s is tagged with @validate which is deprecated and will be removed in TYPO3 v10.0.', $reflectionClass->getName(), $reflectionProperty->getName() ), E_USER_DEPRECATED ); $validatorResolver = GeneralUtility::makeInstance(ValidatorResolver::class); $validateValues = $docCommentParser->getTagValues('validate'); foreach ($validateValues as $validateValue) { $validatorConfiguration = $validatorResolver->parseValidatorAnnotation($validateValue); foreach ($validatorConfiguration['validators'] ?? [] as $validator) { $validatorObjectName = $validatorResolver->resolveValidatorObjectName($validator['validatorName']); $this->properties[$propertyName]['validators'][] = [ 'name' => $validator['validatorName'], 'options' => $validator['validatorOptions'], 'className' => $validatorObjectName, ]; } } } if ($annotationReader->getPropertyAnnotation($reflectionProperty, Lazy::class) instanceof Lazy) { $this->properties[$propertyName]['annotations']['lazy'] = true; } if ($docCommentParser->isTaggedWith('lazy')) { $this->properties[$propertyName]['annotations']['lazy'] = true; trigger_error( sprintf( 'Property %s::%s is tagged with @lazy which is deprecated and will be removed in TYPO3 v10.0.', $reflectionClass->getName(), $reflectionProperty->getName() ), E_USER_DEPRECATED ); } if ($annotationReader->getPropertyAnnotation($reflectionProperty, Transient::class) instanceof Transient) { $this->properties[$propertyName]['annotations']['transient'] = true; } if ($docCommentParser->isTaggedWith('transient')) { $this->properties[$propertyName]['annotations']['transient'] = true; trigger_error( sprintf( 'Property %s::%s is tagged with @transient which is deprecated and will be removed in TYPO3 v10.0.', $reflectionClass->getName(), $reflectionProperty->getName() ), E_USER_DEPRECATED ); } if ($propertyName !== 'settings' && ($annotationReader->getPropertyAnnotation($reflectionProperty, Inject::class) instanceof Inject) ) { try { $varValue = ltrim($docCommentParser->getTagValues('var')[0], '\\'); $this->properties[$propertyName]['annotations']['inject'] = true; $this->properties[$propertyName]['annotations']['type'] = $varValue; $this->properties[$propertyName]['annotations']['dependency'] = $varValue; $this->injectProperties[] = $propertyName; } catch (\Exception $e) { } } if ($propertyName !== 'settings' && $docCommentParser->isTaggedWith('inject')) { trigger_error( sprintf( 'Property %s::%s is tagged with @inject which is deprecated and will be removed in TYPO3 v10.0.', $reflectionClass->getName(), $reflectionProperty->getName() ), E_USER_DEPRECATED ); try { $varValues = $docCommentParser->getTagValues('var'); $this->properties[$propertyName]['annotations']['inject'] = true; $this->properties[$propertyName]['annotations']['type'] = ltrim($varValues[0], '\\'); $this->properties[$propertyName]['annotations']['dependency'] = ltrim($varValues[0], '\\'); if (!$reflectionProperty->isPublic()) { trigger_error( sprintf( 'Property %s::%s is not public and tagged with @inject which is deprecated and will stop working in TYPO3 v10.0.', $reflectionClass->getName(), $reflectionProperty->getName() ), E_USER_DEPRECATED ); } $this->injectProperties[] = $propertyName; } catch (\Exception $e) { } } if ($docCommentParser->isTaggedWith('var') && !$docCommentParser->isTaggedWith('transient')) { try { $cascadeAnnotationValues = $docCommentParser->getTagValues('cascade'); $this->properties[$propertyName]['annotations']['cascade'] = $cascadeAnnotationValues[0]; } catch (\Exception $e) { } if ($this->properties[$propertyName]['annotations']['cascade'] !== null) { trigger_error( sprintf( 'Property %s::%s is tagged with @cascade which is deprecated and will be removed in TYPO3 v10.0.', $reflectionClass->getName(), $reflectionProperty->getName() ), E_USER_DEPRECATED ); } if (($annotation = $annotationReader->getPropertyAnnotation($reflectionProperty, Cascade::class)) instanceof Cascade) { /** @var Cascade $annotation */ $this->properties[$propertyName]['annotations']['cascade'] = $annotation->value; } try { $type = TypeHandlingUtility::parseType(implode(' ', $docCommentParser->getTagValues('var'))); } catch (\Exception $e) { $type = [ 'type' => null, 'elementType' => null ]; } $this->properties[$propertyName]['type'] = $type['type'] ? ltrim($type['type'], '\\') : null; $this->properties[$propertyName]['elementType'] = $type['elementType'] ? ltrim($type['elementType'], '\\') : null; } if ($docCommentParser->isTaggedWith('uuid')) { $this->setUuidPropertyName($propertyName); } if ($docCommentParser->isTaggedWith('identity')) { $this->markAsIdentityProperty($propertyName); } } } /** * @param \ReflectionClass $reflectionClass */ protected function reflectMethods(\ReflectionClass $reflectionClass) { $annotationReader = new AnnotationReader(); foreach ($reflectionClass->getMethods() as $reflectionMethod) { $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]['static'] = $reflectionMethod->isStatic(); $this->methods[$methodName]['abstract'] = $reflectionMethod->isAbstract(); $this->methods[$methodName]['params'] = []; $this->methods[$methodName]['tags'] = []; $this->methods[$methodName]['annotations'] = []; $this->methods[$methodName]['isAction'] = StringUtility::endsWith($methodName, 'Action'); $docCommentParser = new DocCommentParser(true); $docCommentParser->parseDocComment($reflectionMethod->getDocComment()); $argumentValidators = []; $annotations = $annotationReader->getMethodAnnotations($reflectionMethod); /** @var array|Validate[] $validateAnnotations */ $validateAnnotations = array_filter($annotations, function ($annotation) { return $annotation instanceof Validate; }); if ($this->isController && $this->methods[$methodName]['isAction'] && count($validateAnnotations) > 0) { $validatorResolver = GeneralUtility::makeInstance(ValidatorResolver::class); foreach ($validateAnnotations as $validateAnnotation) { $validatorName = $validateAnnotation->validator; $validatorObjectName = $validatorResolver->resolveValidatorObjectName($validatorName); $argumentValidators[$validateAnnotation->param][] = [ 'name' => $validatorName, 'options' => $validateAnnotation->options, 'className' => $validatorObjectName, ]; } } foreach ($docCommentParser->getTagsValues() as $tag => $values) { if ($tag === 'cli') { trigger_error( sprintf( 'Method %s::%s is tagged with @cli which is deprecated and will be removed in TYPO3 v10.0.', $reflectionClass->getName(), $reflectionMethod->getName() ), E_USER_DEPRECATED ); } if ($tag === 'internal' && $reflectionClass->isSubclassOf(\TYPO3\CMS\Extbase\Mvc\Controller\CommandController::class)) { trigger_error( sprintf( 'Command method %s::%s is tagged with @internal which is deprecated and will be removed in TYPO3 v10.0.', $reflectionClass->getName(), $reflectionMethod->getName() ), E_USER_DEPRECATED ); } if ($tag === 'ignorevalidation') { trigger_error( sprintf( 'Method %s::%s is tagged with @ignorevalidation which is deprecated and will be removed in TYPO3 v10.0.', $reflectionClass->getName(), $reflectionMethod->getName() ), E_USER_DEPRECATED ); } if ($tag === 'flushesCaches') { trigger_error( sprintf( 'Method %s::%s is tagged with @flushesCaches which is deprecated and will be removed in TYPO3 v10.0.', $reflectionClass->getName(), $reflectionMethod->getName() ), E_USER_DEPRECATED ); } if ($tag === 'validate' && $this->isController && $this->methods[$methodName]['isAction']) { trigger_error( sprintf( 'Method %s::%s is tagged with @validate which is deprecated and will be removed in TYPO3 v10.0.', $reflectionClass->getName(), $reflectionMethod->getName() ), E_USER_DEPRECATED ); $validatorResolver = GeneralUtility::makeInstance(ValidatorResolver::class); foreach ($values as $validate) { $methodValidatorDefinition = $validatorResolver->parseValidatorAnnotation($validate); foreach ($methodValidatorDefinition['validators'] as $validator) { $validatorObjectName = $validatorResolver->resolveValidatorObjectName($validator['validatorName']); $argumentValidators[$methodValidatorDefinition['argumentName']][] = [ 'name' => $validator['validatorName'], 'options' => $validator['validatorOptions'], 'className' => $validatorObjectName, ]; } } } $this->methods[$methodName]['tags'][$tag] = array_map(function ($value) use ($tag) { // not stripping the dollar sign for @validate annotations is just // a quick fix for a regression introduced in 9.0.0. // This exception to the rules will vanish once the resolving of // validators will take place inside this class and not in the // controller during runtime. return $tag === 'validate' ? $value : ltrim($value, '$'); }, $values); } unset($methodValidatorDefinition); foreach ($annotations as $annotation) { if ($annotation instanceof IgnoreValidation) { $this->methods[$methodName]['tags']['ignorevalidation'][] = $annotation->argumentName; } } $this->methods[$methodName]['description'] = $docCommentParser->getDescription(); foreach ($reflectionMethod->getParameters() as $parameterPosition => $reflectionParameter) { /* @var \ReflectionParameter $reflectionParameter */ $parameterName = $reflectionParameter->getName(); $this->methods[$methodName]['params'][$parameterName] = []; $this->methods[$methodName]['params'][$parameterName]['position'] = $parameterPosition; // compat $this->methods[$methodName]['params'][$parameterName]['byReference'] = $reflectionParameter->isPassedByReference(); // compat $this->methods[$methodName]['params'][$parameterName]['array'] = $reflectionParameter->isArray(); // compat $this->methods[$methodName]['params'][$parameterName]['optional'] = $reflectionParameter->isOptional(); $this->methods[$methodName]['params'][$parameterName]['allowsNull'] = $reflectionParameter->allowsNull(); // compat $this->methods[$methodName]['params'][$parameterName]['class'] = null; // compat $this->methods[$methodName]['params'][$parameterName]['type'] = null; $this->methods[$methodName]['params'][$parameterName]['nullable'] = $reflectionParameter->allowsNull(); $this->methods[$methodName]['params'][$parameterName]['default'] = null; $this->methods[$methodName]['params'][$parameterName]['hasDefaultValue'] = $reflectionParameter->isDefaultValueAvailable(); $this->methods[$methodName]['params'][$parameterName]['defaultValue'] = null; // compat $this->methods[$methodName]['params'][$parameterName]['dependency'] = null; // Extbase DI $this->methods[$methodName]['params'][$parameterName]['validators'] = []; if ($reflectionParameter->isDefaultValueAvailable()) { $this->methods[$methodName]['params'][$parameterName]['default'] = $reflectionParameter->getDefaultValue(); $this->methods[$methodName]['params'][$parameterName]['defaultValue'] = $reflectionParameter->getDefaultValue(); // compat } if (($reflectionType = $reflectionParameter->getType()) instanceof \ReflectionNamedType) { $this->methods[$methodName]['params'][$parameterName]['type'] = $reflectionType->getName(); $this->methods[$methodName]['params'][$parameterName]['nullable'] = $reflectionType->allowsNull(); } if (($parameterClass = $reflectionParameter->getClass()) instanceof \ReflectionClass) { $this->methods[$methodName]['params'][$parameterName]['class'] = $parameterClass->getName(); $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($parameterClass->getName(), '\\'); } else { $methodTagsAndValues = $this->methods[$methodName]['tags']; if (isset($methodTagsAndValues['param'][$parameterPosition])) { $explodedParameters = explode(' ', $methodTagsAndValues['param'][$parameterPosition]); if (count($explodedParameters) >= 2) { if (TypeHandlingUtility::isSimpleType($explodedParameters[0])) { // ensure that short names of simple types are resolved correctly to the long form // this is important for all kinds of type checks later on $typeInfo = TypeHandlingUtility::parseType($explodedParameters[0]); $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($typeInfo['type'], '\\'); } else { $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($explodedParameters[0], '\\'); } } } } // Extbase DI if ($reflectionParameter->getClass() instanceof \ReflectionClass && ($reflectionMethod->isConstructor() || $this->hasInjectMethodName($reflectionMethod)) ) { $this->methods[$methodName]['params'][$parameterName]['dependency'] = $reflectionParameter->getClass()->getName(); } // Extbase Validation if (isset($argumentValidators[$parameterName])) { if ($this->methods[$methodName]['params'][$parameterName]['type'] === null) { throw new InvalidTypeHintException( 'Missing type information for parameter "$' . $parameterName . '" in ' . $this->className . '->' . $methodName . '(): Either use an @param annotation or use a type hint.', 1515075192 ); } $this->methods[$methodName]['params'][$parameterName]['validators'] = $argumentValidators[$parameterName]; unset($argumentValidators[$parameterName]); } } // Extbase Validation foreach ($argumentValidators as $parameterName => $validators) { $validatorNames = array_column($validators, 'name'); throw new InvalidValidationConfigurationException( 'Invalid validate annotation in ' . $this->className . '->' . $methodName . '(): The following validators have been defined for missing param "$' . $parameterName . '": ' . implode(', ', $validatorNames), 1515073585 ); } // Extbase $this->methods[$methodName]['injectMethod'] = false; if ($this->hasInjectMethodName($reflectionMethod) && count($this->methods[$methodName]['params']) === 1 && reset($this->methods[$methodName]['params'])['dependency'] !== null ) { $this->methods[$methodName]['injectMethod'] = true; $this->injectMethods[] = $methodName; } } } /** * Returns the class name this schema is referring to * * @return string The class name */ public function getClassName(): string { return $this->className; } /** * Adds (defines) a specific property and its type. * * @param string $name Name of the property * @param string $type Type of the property * @param bool $lazy Whether the property should be lazy-loaded when reconstituting * @param string $cascade Strategy to cascade the object graph. * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. */ public function addProperty($name, $type, $lazy = false, $cascade = '') { trigger_error( 'This method will be removed in TYPO3 v10.0, properties will be automatically added on ClassSchema construction.', E_USER_DEPRECATED ); $type = TypeHandlingUtility::parseType($type); $this->properties[$name] = [ 'type' => $type['type'], 'elementType' => $type['elementType'], 'lazy' => $lazy, 'cascade' => $cascade ]; } /** * Returns the given property defined in this schema. Check with * hasProperty($propertyName) before! * * @param string $propertyName * @return array */ public function getProperty($propertyName) { return isset($this->properties[$propertyName]) && is_array($this->properties[$propertyName]) ? $this->properties[$propertyName] : []; } /** * Returns all properties defined in this schema * * @return array */ public function getProperties() { return $this->properties; } /** * Sets the model type of the class this schema is referring to. * * @param int $modelType The model type, one of the MODELTYPE_* constants. * @throws \InvalidArgumentException * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. */ public function setModelType($modelType) { trigger_error( 'This method will be removed in TYPO3 v10.0, modelType will be automatically set on ClassSchema construction.', E_USER_DEPRECATED ); if ($modelType < self::MODELTYPE_ENTITY || $modelType > self::MODELTYPE_VALUEOBJECT) { throw new \InvalidArgumentException('"' . $modelType . '" is an invalid model type.', 1212519195); } $this->modelType = $modelType; } /** * Returns the model type of the class this schema is referring to. * * @return int The model type, one of the MODELTYPE_* constants. * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. */ public function getModelType() { trigger_error( 'This method will be removed in TYPO3 v10.0.', E_USER_DEPRECATED ); return $this->modelType; } /** * Marks the class if it is root of an aggregate and therefore accessible * through a repository - or not. * * @param bool $isRoot TRUE if it is the root of an aggregate */ public function setAggregateRoot($isRoot) { $this->aggregateRoot = $isRoot; } /** * Whether the class is an aggregate root and therefore accessible through * a repository. * * @return bool TRUE if it is managed */ public function isAggregateRoot(): bool { return $this->aggregateRoot; } /** * If the class schema has a certain property. * * @param string $propertyName Name of the property * @return bool */ public function hasProperty($propertyName): bool { return array_key_exists($propertyName, $this->properties); } /** * Sets the property marked as uuid of an object with @uuid * * @param string $propertyName * @throws \InvalidArgumentException * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. */ public function setUuidPropertyName($propertyName) { trigger_error( 'Tagging properties with @uuid is deprecated and will be removed in TYPO3 v10.0.', E_USER_DEPRECATED ); if (!array_key_exists($propertyName, $this->properties)) { throw new \InvalidArgumentException('Property "' . $propertyName . '" must be added to the class schema before it can be marked as UUID property.', 1233863842); } $this->uuidPropertyName = $propertyName; } /** * Gets the name of the property marked as uuid of an object * * @return string * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. */ public function getUuidPropertyName() { trigger_error( 'Tagging properties with @uuid is deprecated and will be removed in TYPO3 v10.0.', E_USER_DEPRECATED ); return $this->uuidPropertyName; } /** * Marks the given property as one of properties forming the identity * of an object. The property must already be registered in the class * schema. * * @param string $propertyName * @throws \InvalidArgumentException * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. */ public function markAsIdentityProperty($propertyName) { trigger_error( 'Tagging properties with @identity is deprecated and will be removed in TYPO3 v10.0.', E_USER_DEPRECATED ); if (!array_key_exists($propertyName, $this->properties)) { throw new \InvalidArgumentException('Property "' . $propertyName . '" must be added to the class schema before it can be marked as identity property.', 1233775407); } if ($this->properties[$propertyName]['annotations']['lazy'] === true) { throw new \InvalidArgumentException('Property "' . $propertyName . '" must not be makred for lazy loading to be marked as identity property.', 1239896904); } $this->identityProperties[$propertyName] = $this->properties[$propertyName]['type']; } /** * Gets the properties (names and types) forming the identity of an object. * * @return array * @see markAsIdentityProperty() * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. */ public function getIdentityProperties() { trigger_error( 'Tagging properties with @identity is deprecated and will be removed in TYPO3 v10.0.', E_USER_DEPRECATED ); return $this->identityProperties; } /** * @return bool */ public function hasConstructor(): bool { return isset($this->methods['__construct']); } /** * @param string $name * @return array */ public function getMethod(string $name): array { return $this->methods[$name] ?? []; } /** * @return array */ public function getMethods(): array { return $this->methods; } /** * @param \ReflectionMethod $reflectionMethod * @return bool */ protected function hasInjectMethodName(\ReflectionMethod $reflectionMethod): bool { $methodName = $reflectionMethod->getName(); if ($methodName === 'injectSettings' || !$reflectionMethod->isPublic()) { return false; } if ( strpos($reflectionMethod->getName(), 'inject') === 0 ) { return true; } return false; } /** * @return bool * @internal */ public function isModel(): bool { return $this->isEntity() || $this->isValueObject(); } /** * @return bool * @internal */ public function isEntity(): bool { return $this->modelType === static::MODELTYPE_ENTITY; } /** * @return bool * @internal */ public function isValueObject(): bool { return $this->modelType === static::MODELTYPE_VALUEOBJECT; } /** * @return bool */ public function isSingleton(): bool { return $this->isSingleton; } /** * @param string $methodName * @return bool */ public function hasMethod(string $methodName): bool { return isset($this->methods[$methodName]); } /** * @return array */ public function getTags(): array { return $this->tags; } /** * @return bool */ public function hasInjectProperties(): bool { return count($this->injectProperties) > 0; } /** * @return bool */ public function hasInjectMethods(): bool { return count($this->injectMethods) > 0; } /** * @return array */ public function getInjectMethods(): array { $injectMethods = []; foreach ($this->injectMethods as $injectMethodName) { $injectMethods[$injectMethodName] = reset($this->methods[$injectMethodName]['params'])['dependency']; } return $injectMethods; } /** * @return array */ public function getInjectProperties(): array { $injectProperties = []; foreach ($this->injectProperties as $injectPropertyName) { $injectProperties[$injectPropertyName] = $this->properties[$injectPropertyName]['annotations']['dependency']; } return $injectProperties; } /** * @return array */ public function getConstructorArguments(): array { if (!$this->hasConstructor()) { return []; } return $this->methods['__construct']['params']; } }