From 84879ed47fb1b01f6e6334102a906cd65b03cd47 Mon Sep 17 00:00:00 2001 From: Alexander Schnitzler Date: Thu, 4 Jan 2018 11:34:42 +0100 Subject: [PATCH] [!!!][TASK] Aggregate validator information in class schema This is the first part of many to streamline the resolving of validators. In this patch, the following changes: - The class schema aggregates all the information about validators that are added via @validate annotations. As reflection is involved here, it makes sense to put this into the class schema generation and remove it from the ActionController. - Along with this change there have been changes to the ValidatorResolver class. Being references only in the ActionController, buildMethodArgumentsValidatorConjunctions has been deprecated and is no longer used by the core itself. - Also, the methods parseValidatorAnnotation and resolveValidatorObjectName have been made public as they are now used from outside the ValidatorResolver class. The main achievements of this patch are getting rid of runtime reflection by the ActionController and fetching the necessary information about validators from the class schema, which at this very moment, is also generated during runtime but is cached and that cache can be warmed up in the future. Therefore this change does also improve the runtime performance of Extbase a bit. This patch is considered breaking as it removes the support for adding validators to properties of method arguments via the following (quite unknown) semantic sugar. /* * @param Model $model * @validate $model.property NotEmpty */ public function foo(Model $model){} This possibility is quite unknown and unused in the wild and as it eases the aggregation of validators it will be removed without any replacement. However, whenever a model is validated and a model validator exists for that model, it will be registered and called automatically. If not dealing with models but regular objects or arrays, the recommended way is to write a custom validator and do the validation manually in that class. Releases: master Resolves: #83475 Change-Id: I3c76e722fe084e8346bb27ea5ba8c7ef0f056eda Reviewed-on: https://review.typo3.org/55261 Reviewed-by: Christian Kuhn Tested-by: Christian Kuhn Reviewed-by: Frank Naegler Tested-by: Frank Naegler --- ...egateValidatorInformationInClassSchema.rst | 55 +++++++ ...ateValidatorInformationInClassSchema-1.rst | 41 ++++++ ...ateValidatorInformationInClassSchema-2.rst | 32 ++++ .../Mvc/Controller/ActionController.php | 48 ++++-- .../Classes/Reflection/ClassSchema.php | 80 +++++++++- .../Classes/Validation/ValidatorResolver.php | 13 +- .../Tests/Unit/Reflection/ClassSchemaTest.php | 118 +++++++++++++-- .../Reflection/Fixture/DummyController.php | 44 ++++++ ...lerWithValidateAnnotationWithoutParam.php} | 24 +-- ...ValidateAnnotationWithoutParamTypeHint.php | 30 ++++ .../Unit/Reflection/Fixture/DummyModel.php | 11 ++ .../Validation/Validator/DummyValidator.php | 41 ++++++ .../Unit/Reflection/ReflectionServiceTest.php | 3 + .../Validation/ValidatorResolverTest.php | 137 ++++++++++++++++++ .../Php/MethodCallMatcher.php | 7 + .../Php/MethodCallStaticMatcher.php | 7 + 16 files changed, 639 insertions(+), 52 deletions(-) create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Breaking-83475-AggregateValidatorInformationInClassSchema.rst create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Deprecation-83475-AggregateValidatorInformationInClassSchema-1.rst create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Deprecation-83475-AggregateValidatorInformationInClassSchema-2.rst create mode 100644 typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/DummyController.php rename typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/{DummyClassWithValidateAnnotation.php => DummyControllerWithValidateAnnotationWithoutParam.php} (50%) create mode 100644 typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/DummyControllerWithValidateAnnotationWithoutParamTypeHint.php create mode 100644 typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/Validation/Validator/DummyValidator.php create mode 100644 typo3/sysext/extbase/Tests/UnitDeprecated/Validation/ValidatorResolverTest.php diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-83475-AggregateValidatorInformationInClassSchema.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-83475-AggregateValidatorInformationInClassSchema.rst new file mode 100644 index 000000000000..550ced0716f6 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Breaking-83475-AggregateValidatorInformationInClassSchema.rst @@ -0,0 +1,55 @@ +.. include:: ../../Includes.txt + +================================================================== +Breaking: #83475 - Aggregate validator information in class schema +================================================================== + +See :issue:`83475` + +Description +=========== + +It is no longer possible to use the following semantic sugar to define validators for properties of action parameters: + +.. code-block:: php + + /* + * @param Model $model + * @validate $model.property NotEmpty + */ + public function foo(Model $model){} + +Mind the dot and the reference to the property. This will no longer work. +Of course, the regular validation of action parameters stays intact. + +.. code-block:: php + + /* + * @param Model $model + * @validate $model CustomValidator + */ + public function foo(Model $model){} + +This will continue to work. + + +Impact +====== + +If you rely on that feature, you need to manually implement the validation in the future. + + +Affected Installations +====================== + +All installations that use that feature. + + +Migration +========= + +If you used that feature for adding validators to models, you can define the validators inside the model instead or inside a model validator, that is automatically registered and loaded if defined. + +When using that feature with regular objects, you need to write custom validators and call the desired property validators in there. + +.. index:: NotScanned diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-83475-AggregateValidatorInformationInClassSchema-1.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-83475-AggregateValidatorInformationInClassSchema-1.rst new file mode 100644 index 000000000000..a5a88b60f800 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-83475-AggregateValidatorInformationInClassSchema-1.rst @@ -0,0 +1,41 @@ +.. include:: ../../Includes.txt + +===================================================================== +Deprecation: #83475 - Aggregate validator information in class schema +===================================================================== + +See :issue:`83475` + +Description +=========== + +The method `\TYPO3\CMS\Extbase\Mvc\Controller\ActionController::getActionMethodParameters` is deprecated and will be removed in TYPO3 v10.0 + + +Impact +====== + +The method is not considered public api and it is unlikely that the methods is used in the wild. If you rely on that method, please migrate your code base. + + +Affected Installations +====================== + +All installations that use that method. + + +Migration +========= + +Use the ClassSchema class and get all necessary information from it. +Example: + +.. code-block:: php + + $reflectionService = $objectManager->get(\TYPO3\CMS\Extbase\Reflection\ReflectionService::class); + $methods = $reflectionService->getClassSchema($className)->getMethods(); + $actions = array_filter($methods, function($method){ + return $method['isAction']; + }); + +.. index:: PHP-API, FullyScanned diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-83475-AggregateValidatorInformationInClassSchema-2.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-83475-AggregateValidatorInformationInClassSchema-2.rst new file mode 100644 index 000000000000..180d6aebad33 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-83475-AggregateValidatorInformationInClassSchema-2.rst @@ -0,0 +1,32 @@ +.. include:: ../../Includes.txt + +===================================================================== +Deprecation: #83475 - Aggregate validator information in class schema +===================================================================== + +See :issue:`83475` + +Description +=========== + +The method `\TYPO3\CMS\Extbase\Validation\ValidatorResolver::buildMethodArgumentsValidatorConjunctions` is deprecated and will be removed in TYPO3 v10.0 + + +Impact +====== + +The method is not considered public api and it is unlikely that the methods is used in the wild. If you rely on that method, you will need to implement the logic yourself. + + +Affected Installations +====================== + +All installations that use that method. + + +Migration +========= + +There is no migration + +.. index:: PHP-API, FullyScanned diff --git a/typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php b/typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php index d5af3a1a4d03..82854de303bc 100644 --- a/typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php +++ b/typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php @@ -21,6 +21,7 @@ use TYPO3\CMS\Extbase\Mvc\View\ViewInterface; use TYPO3\CMS\Extbase\Mvc\Web\Request as WebRequest; use TYPO3\CMS\Extbase\Validation\Validator\AbstractCompositeValidator; +use TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator; use TYPO3Fluid\Fluid\View\TemplateView; /** @@ -250,26 +251,37 @@ protected function initializeActionMethodArguments() */ protected function initializeActionMethodValidators() { + $methodParameters = $this->reflectionService->getMethodParameters(static::class, $this->actionMethodName); - /** - * @todo: add validation group support - * (https://review.typo3.org/#/c/13556/4) - */ - $actionMethodParameters = static::getActionMethodParameters($this->objectManager); - if (isset($actionMethodParameters[$this->actionMethodName])) { - $methodParameters = $actionMethodParameters[$this->actionMethodName]; - } else { - $methodParameters = []; + /** @var ConjunctionValidator[] $validatorConjunctions */ + $validatorConjunctions = []; + foreach ($methodParameters as $parameterName => $methodParameter) { + /** @var ConjunctionValidator $validatorConjunction */ + $validatorConjunction = $this->objectManager->get(ConjunctionValidator::class); + + // @todo: remove check for old underscore model name syntax once it's possible + if (strpbrk($methodParameter['type'], '_\\') === false) { + // this checks if the type is a simply type and then adds a + // validator. StringValidator and such for example. + $typeValidator = $this->validatorResolver->createValidator($methodParameter['type']); + + if ($typeValidator !== null) { + $validatorConjunction->addValidator($typeValidator); + } + } + + $validatorConjunctions[$parameterName] = $validatorConjunction; + + foreach ($methodParameter['validators'] as $validator) { + $validatorConjunctions[$parameterName]->addValidator( + $this->objectManager->get($validator['className'], $validator['options']) + ); + } } - /** - * @todo: add resolving of $actionValidateAnnotations and pass them to - * buildMethodArgumentsValidatorConjunctions as in TYPO3.Flow - */ - $parameterValidators = $this->validatorResolver->buildMethodArgumentsValidatorConjunctions(static::class, $this->actionMethodName, $methodParameters); /** @var \TYPO3\CMS\Extbase\Mvc\Controller\Argument $argument */ foreach ($this->arguments as $argument) { - $validator = $parameterValidators[$argument->getName()]; + $validator = $validatorConjunctions[$argument->getName()]; $baseValidatorConjunction = $this->validatorResolver->getBaseValidatorConjunction($argument->getDataType()); if (!empty($baseValidatorConjunction) && $validator instanceof AbstractCompositeValidator) { @@ -636,9 +648,15 @@ protected function getFlattenedValidationErrorMessage() * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager * * @return array Array of method parameters by action name + * @deprecated */ public static function getActionMethodParameters($objectManager) { + trigger_error( + 'Method ' . __METHOD__ . ' is deprecated and will be removed in TYPO3 v10.0.', + E_USER_DEPRECATED + ); + $reflectionService = $objectManager->get(\TYPO3\CMS\Extbase\Reflection\ReflectionService::class); $result = []; diff --git a/typo3/sysext/extbase/Classes/Reflection/ClassSchema.php b/typo3/sysext/extbase/Classes/Reflection/ClassSchema.php index 3c7f681d808c..4eb41908c8fb 100644 --- a/typo3/sysext/extbase/Classes/Reflection/ClassSchema.php +++ b/typo3/sysext/extbase/Classes/Reflection/ClassSchema.php @@ -17,6 +17,8 @@ use Doctrine\Common\Annotations\AnnotationReader; use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Utility\ClassNamingUtility; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\StringUtility; use TYPO3\CMS\Extbase\Annotation\IgnoreValidation; use TYPO3\CMS\Extbase\Annotation\Inject; use TYPO3\CMS\Extbase\Annotation\ORM\Cascade; @@ -24,7 +26,11 @@ use TYPO3\CMS\Extbase\Annotation\ORM\Transient; use TYPO3\CMS\Extbase\DomainObject\AbstractEntity; use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject; +use TYPO3\CMS\Extbase\Mvc\Controller\ControllerInterface; use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility; +use TYPO3\CMS\Extbase\Validation\Exception\InvalidTypeHintException; +use TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationConfigurationException; +use TYPO3\CMS\Extbase\Validation\ValidatorResolver; /** * A class schema @@ -89,6 +95,11 @@ class ClassSchema */ private $isSingleton; + /** + * @var bool + */ + private $isController; + /** * @var array */ @@ -123,6 +134,7 @@ public function __construct($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; @@ -164,7 +176,8 @@ protected function reflectProperties(\ReflectionClass $reflectionClass) 'type' => null, // Extbase 'elementType' => null, // Extbase 'annotations' => [], - 'tags' => [] + 'tags' => [], + 'validators' => [] ]; $docCommentParser = new DocCommentParser(true); @@ -179,10 +192,24 @@ protected function reflectProperties(\ReflectionClass $reflectionClass) $this->properties[$propertyName]['annotations']['type'] = null; $this->properties[$propertyName]['annotations']['cascade'] = null; $this->properties[$propertyName]['annotations']['dependency'] = null; - $this->properties[$propertyName]['annotations']['validators'] = []; if ($docCommentParser->isTaggedWith('validate')) { - $this->properties[$propertyName]['annotations']['validators'] = $docCommentParser->getTagValues('validate'); + $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) { @@ -307,12 +334,12 @@ protected function reflectMethods(\ReflectionClass $reflectionClass) $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()); - $this->methods[$methodName]['annotations']['validators'] = []; - + $argumentValidators = []; foreach ($docCommentParser->getTagsValues() as $tag => $values) { if ($tag === 'ignorevalidation') { trigger_error( @@ -320,8 +347,22 @@ protected function reflectMethods(\ReflectionClass $reflectionClass) E_USER_DEPRECATED ); } - if ($tag === 'validate') { - $this->methods[$methodName]['annotations']['validators'] = $values; + if ($tag === 'validate' && $this->isController && $this->methods[$methodName]['isAction']) { + $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 @@ -332,6 +373,7 @@ protected function reflectMethods(\ReflectionClass $reflectionClass) return $tag === 'validate' ? $value : ltrim($value, '$'); }, $values); } + unset($methodValidatorDefinition); foreach ($annotationReader->getMethodAnnotations($reflectionMethod) as $annotation) { if ($annotation instanceof IgnoreValidation) { @@ -359,6 +401,7 @@ protected function reflectMethods(\ReflectionClass $reflectionClass) $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(); @@ -397,6 +440,29 @@ protected function reflectMethods(\ReflectionClass $reflectionClass) ) { $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 diff --git a/typo3/sysext/extbase/Classes/Validation/ValidatorResolver.php b/typo3/sysext/extbase/Classes/Validation/ValidatorResolver.php index 7e0d5a58e27e..c55789b6fdd2 100644 --- a/typo3/sysext/extbase/Classes/Validation/ValidatorResolver.php +++ b/typo3/sysext/extbase/Classes/Validation/ValidatorResolver.php @@ -110,6 +110,7 @@ public function createValidator($validatorType, array $validatorOptions = []) $validator = $this->objectManager->get($validatorObjectName, $validatorOptions); + // Move this check into ClassSchema if (!($validator instanceof \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface)) { throw new Exception\NoSuchValidatorException('The validator "' . $validatorObjectName . '" does not implement TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface!', 1300694875); } @@ -152,9 +153,15 @@ public function getBaseValidatorConjunction($targetClassName) * @throws \TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationConfigurationException * @throws \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException * @throws \TYPO3\CMS\Extbase\Validation\Exception\InvalidTypeHintException + * @deprecated */ public function buildMethodArgumentsValidatorConjunctions($className, $methodName, array $methodParameters = null, array $methodValidateAnnotations = null) { + trigger_error( + 'Method ' . __METHOD__ . ' is deprecated and will be removed in TYPO3 v10.0.', + E_USER_DEPRECATED + ); + /** @var ConjunctionValidator[] $validatorConjunctions */ $validatorConjunctions = []; @@ -362,8 +369,9 @@ protected function addCustomValidators($targetClassName, ConjunctionValidator &$ * * @param string $validateValue * @return array + * @internal */ - protected function parseValidatorAnnotation($validateValue) + public function parseValidatorAnnotation($validateValue) { $matches = []; if ($validateValue[0] === '$') { @@ -432,8 +440,9 @@ protected function unquoteString(&$quotedValue) * * @throws Exception\NoSuchValidatorException * @return string Name of the validator object + * @internal */ - protected function resolveValidatorObjectName($validatorName) + public function resolveValidatorObjectName($validatorName) { if (strpos($validatorName, ':') !== false) { // Found shorthand validator, either extbase or foreign extension diff --git a/typo3/sysext/extbase/Tests/Unit/Reflection/ClassSchemaTest.php b/typo3/sysext/extbase/Tests/Unit/Reflection/ClassSchemaTest.php index 7d03fab6774e..cb26c2002f6e 100644 --- a/typo3/sysext/extbase/Tests/Unit/Reflection/ClassSchemaTest.php +++ b/typo3/sysext/extbase/Tests/Unit/Reflection/ClassSchemaTest.php @@ -17,6 +17,10 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Persistence\ObjectStorage; use TYPO3\CMS\Extbase\Reflection\ClassSchema; +use TYPO3\CMS\Extbase\Validation\Exception\InvalidTypeHintException; +use TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationConfigurationException; +use TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator; +use TYPO3\CMS\Extbase\Validation\Validator\StringLengthValidator; /** * Test case @@ -351,33 +355,127 @@ public function testClassSchemaGetTags() /** * @test */ - public function classSchemaDetectsValidateAnnotation() + public function classSchemaDetectsValidateAnnotationsModelProperties() { - $classSchema = new ClassSchema(Fixture\DummyClassWithValidateAnnotation::class); + $classSchema = new ClassSchema(Fixture\DummyModel::class); static::assertSame( [], - $classSchema->getProperty('propertyWithoutValidateAnnotations')['annotations']['validators'] + $classSchema->getProperty('propertyWithoutValidateAnnotations')['validators'] ); static::assertSame( [ - 'NotEmpty', - 'Empty (Foo=Bar)' + [ + 'name' => 'StringLength', + 'options' => [ + 'minimum' => '1', + 'maximum' => '10', + ], + 'className' => StringLengthValidator::class + ], + [ + 'name' => 'NotEmpty', + 'options' => [], + 'className' => NotEmptyValidator::class + ], + [ + 'name' => 'TYPO3.CMS.Extbase:NotEmpty', + 'options' => [], + 'className' => NotEmptyValidator::class + ], + [ + 'name' => 'TYPO3.CMS.Extbase.Tests.Unit.Reflection.Fixture:DummyValidator', + 'options' => [], + 'className' => Fixture\Validation\Validator\DummyValidator::class + ], + [ + 'name' => '\TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator', + 'options' => [], + 'className' => NotEmptyValidator::class + ], + [ + 'name' => NotEmptyValidator::class, + 'options' => [], + 'className' => NotEmptyValidator::class + ] ], - $classSchema->getProperty('propertyWithValidateAnnotations')['annotations']['validators'] + $classSchema->getProperty('propertyWithValidateAnnotations')['validators'] ); + } + + /** + * @test + */ + public function classSchemaDetectsValidateAnnotationsOfControllerActions() + { + $classSchema = new ClassSchema(Fixture\DummyController::class); static::assertSame( [], - $classSchema->getMethod('methodWithoutValidateAnnotations')['annotations']['validators'] + $classSchema->getMethod('methodWithoutValidateAnnotationsAction')['params']['fooParam']['validators'] ); static::assertSame( [ - '$fooParam FooValidator (FooValidatorOptionKey=FooValidatorOptionValue)', - '$fooParam BarValidator' + [ + 'name' => 'StringLength', + 'options' => [ + 'minimum' => '1', + 'maximum' => '10', + ], + 'className' => StringLengthValidator::class + ], + [ + 'name' => 'NotEmpty', + 'options' => [], + 'className' => NotEmptyValidator::class + ], + [ + 'name' => 'TYPO3.CMS.Extbase:NotEmpty', + 'options' => [], + 'className' => NotEmptyValidator::class + ], + [ + 'name' => 'TYPO3.CMS.Extbase.Tests.Unit.Reflection.Fixture:DummyValidator', + 'options' => [], + 'className' => Fixture\Validation\Validator\DummyValidator::class + ], + [ + 'name' => '\TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator', + 'options' => [], + 'className' => NotEmptyValidator::class + ], + [ + 'name' => NotEmptyValidator::class, + 'options' => [], + 'className' => NotEmptyValidator::class + ] ], - $classSchema->getMethod('methodWithValidateAnnotations')['annotations']['validators'] + $classSchema->getMethod('methodWithValidateAnnotationsAction')['params']['fooParam']['validators'] ); } + + /** + * @test + */ + public function classSchemaGenerationThrowsExceptionWithValidateAnnotationsForParamWithoutTypeHint() + { + $this->expectException(InvalidTypeHintException::class); + $this->expectExceptionMessage('Missing type information for parameter "$fooParam" in TYPO3\CMS\Extbase\Tests\Unit\Reflection\Fixture\DummyControllerWithValidateAnnotationWithoutParamTypeHint->methodWithValidateAnnotationsAction(): Either use an @param annotation or use a type hint.'); + $this->expectExceptionCode(1515075192); + + new ClassSchema(Fixture\DummyControllerWithValidateAnnotationWithoutParamTypeHint::class); + } + + /** + * @test + */ + public function classSchemaGenerationThrowsExceptionWithValidateAnnotationsForMissingParam() + { + $this->expectException(InvalidValidationConfigurationException::class); + $this->expectExceptionMessage('Invalid validate annotation in TYPO3\CMS\Extbase\Tests\Unit\Reflection\Fixture\DummyControllerWithValidateAnnotationWithoutParam->methodWithValidateAnnotationsAction(): The following validators have been defined for missing param "$fooParam": NotEmpty, StringLength'); + $this->expectExceptionCode(1515073585); + + new ClassSchema(Fixture\DummyControllerWithValidateAnnotationWithoutParam::class); + } } diff --git a/typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/DummyController.php b/typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/DummyController.php new file mode 100644 index 000000000000..308bdcd23ba2 --- /dev/null +++ b/typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/DummyController.php @@ -0,0 +1,44 @@ + false, 'defaultValue' => null, 'dependency' => null, + 'validators' => [], ] ], $parameters); @@ -175,6 +176,7 @@ public function getMethodParametersWithShortTypeNames() 'hasDefaultValue' => false, 'defaultValue' => null, 'dependency' => null, + 'validators' => [], ], 'foo' => [ 'position' => 1, @@ -189,6 +191,7 @@ public function getMethodParametersWithShortTypeNames() 'hasDefaultValue' => false, 'defaultValue' => null, 'dependency' => null, + 'validators' => [], ] ], $parameters); } diff --git a/typo3/sysext/extbase/Tests/UnitDeprecated/Validation/ValidatorResolverTest.php b/typo3/sysext/extbase/Tests/UnitDeprecated/Validation/ValidatorResolverTest.php new file mode 100644 index 000000000000..fd31d32b69d7 --- /dev/null +++ b/typo3/sysext/extbase/Tests/UnitDeprecated/Validation/ValidatorResolverTest.php @@ -0,0 +1,137 @@ +getAccessibleMock(\TYPO3\CMS\Extbase\Mvc\Controller\ActionController::class, ['fooAction'], [], '', false); + $methodParameters = []; + $mockReflectionService = $this->createMock(\TYPO3\CMS\Extbase\Reflection\ReflectionService::class); + $mockReflectionService->expects($this->once())->method('getMethodParameters')->with(get_class($mockController), 'fooAction')->will($this->returnValue($methodParameters)); + $validatorResolver = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Validation\ValidatorResolver::class, ['createValidator']); + $validatorResolver->_set('reflectionService', $mockReflectionService); + $result = $validatorResolver->buildMethodArgumentsValidatorConjunctions(get_class($mockController), 'fooAction'); + $this->assertSame([], $result); + } + + /** + * @test + */ + public function buildMethodArgumentsValidatorConjunctionsBuildsAConjunctionFromValidateAnnotationsOfTheSpecifiedMethod() + { + $mockObject = $this->getMockBuilder('stdClass') + ->setMethods(['fooMethod']) + ->disableOriginalConstructor() + ->getMock(); + $methodParameters = [ + 'arg1' => [ + 'type' => 'string' + ], + 'arg2' => [ + 'type' => 'array' + ] + ]; + $methodTagsValues = [ + 'param' => [ + 'string $arg1', + 'array $arg2' + ], + 'validate' => [ + '$arg1 Foo(bar = baz), Bar', + '$arg2 VENDOR\\ModelCollection\\Domain\\Model\\Model' + ] + ]; + $mockReflectionService = $this->createMock(\TYPO3\CMS\Extbase\Reflection\ReflectionService::class); + $mockReflectionService->expects($this->once())->method('getMethodTagsValues')->with(get_class($mockObject), 'fooAction')->will($this->returnValue($methodTagsValues)); + $mockReflectionService->expects($this->once())->method('getMethodParameters')->with(get_class($mockObject), 'fooAction')->will($this->returnValue($methodParameters)); + $mockStringValidator = $this->createMock(\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface::class); + $mockArrayValidator = $this->createMock(\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface::class); + $mockFooValidator = $this->createMock(\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface::class); + $mockBarValidator = $this->createMock(\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface::class); + $mockQuuxValidator = $this->createMock(\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface::class); + $conjunction1 = $this->createMock(\TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator::class); + $conjunction1->expects($this->at(0))->method('addValidator')->with($mockStringValidator); + $conjunction1->expects($this->at(1))->method('addValidator')->with($mockFooValidator); + $conjunction1->expects($this->at(2))->method('addValidator')->with($mockBarValidator); + $conjunction2 = $this->createMock(\TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator::class); + $conjunction2->expects($this->at(0))->method('addValidator')->with($mockArrayValidator); + $conjunction2->expects($this->at(1))->method('addValidator')->with($mockQuuxValidator); + $mockArguments = new \TYPO3\CMS\Extbase\Mvc\Controller\Arguments(); + $mockArguments->addArgument(new \TYPO3\CMS\Extbase\Mvc\Controller\Argument('arg1', 'dummyValue')); + $mockArguments->addArgument(new \TYPO3\CMS\Extbase\Mvc\Controller\Argument('arg2', 'dummyValue')); + $validatorResolver = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Validation\ValidatorResolver::class, ['createValidator']); + $validatorResolver->expects($this->at(0))->method('createValidator')->with(\TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator::class)->will($this->returnValue($conjunction1)); + $validatorResolver->expects($this->at(1))->method('createValidator')->with('string')->will($this->returnValue($mockStringValidator)); + $validatorResolver->expects($this->at(2))->method('createValidator')->with(\TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator::class)->will($this->returnValue($conjunction2)); + $validatorResolver->expects($this->at(3))->method('createValidator')->with('array')->will($this->returnValue($mockArrayValidator)); + $validatorResolver->expects($this->at(4))->method('createValidator')->with('Foo', ['bar' => 'baz'])->will($this->returnValue($mockFooValidator)); + $validatorResolver->expects($this->at(5))->method('createValidator')->with('Bar')->will($this->returnValue($mockBarValidator)); + $validatorResolver->expects($this->at(6))->method('createValidator')->with('VENDOR\\ModelCollection\\Domain\\Model\\Model')->will($this->returnValue($mockQuuxValidator)); + $validatorResolver->_set('reflectionService', $mockReflectionService); + $result = $validatorResolver->buildMethodArgumentsValidatorConjunctions(get_class($mockObject), 'fooAction'); + $this->assertEquals(['arg1' => $conjunction1, 'arg2' => $conjunction2], $result); + } + + /** + * @test + */ + public function buildMethodArgumentsValidatorConjunctionsThrowsExceptionIfValidationAnnotationForNonExistingArgumentExists() + { + $this->expectException(InvalidValidationConfigurationException::class); + $this->expectExceptionCode(1253172726); + $mockObject = $this->getMockBuilder('stdClass') + ->setMethods(['fooMethod']) + ->disableOriginalConstructor() + ->getMock(); + $methodParameters = [ + 'arg1' => [ + 'type' => 'string' + ] + ]; + $methodTagsValues = [ + 'param' => [ + 'string $arg1' + ], + 'validate' => [ + '$arg2 VENDOR\\ModelCollection\\Domain\\Model\\Model' + ] + ]; + $mockReflectionService = $this->createMock(\TYPO3\CMS\Extbase\Reflection\ReflectionService::class); + $mockReflectionService->expects($this->once())->method('getMethodTagsValues')->with(get_class($mockObject), 'fooAction')->will($this->returnValue($methodTagsValues)); + $mockReflectionService->expects($this->once())->method('getMethodParameters')->with(get_class($mockObject), 'fooAction')->will($this->returnValue($methodParameters)); + $mockStringValidator = $this->createMock(\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface::class); + $mockQuuxValidator = $this->createMock(\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface::class); + $conjunction1 = $this->createMock(\TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator::class); + $conjunction1->expects($this->at(0))->method('addValidator')->with($mockStringValidator); + $validatorResolver = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Validation\ValidatorResolver::class, ['createValidator']); + $validatorResolver->expects($this->at(0))->method('createValidator')->with(\TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator::class)->will($this->returnValue($conjunction1)); + $validatorResolver->expects($this->at(1))->method('createValidator')->with('string')->will($this->returnValue($mockStringValidator)); + $validatorResolver->expects($this->at(2))->method('createValidator')->with('VENDOR\\ModelCollection\\Domain\\Model\\Model')->will($this->returnValue($mockQuuxValidator)); + $validatorResolver->_set('reflectionService', $mockReflectionService); + $validatorResolver->buildMethodArgumentsValidatorConjunctions(get_class($mockObject), 'fooAction'); + } +} diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php index 405cce2d379d..94a0a74ff5e4 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php @@ -2025,4 +2025,11 @@ 'Deprecation-84407-AJAXRequestMethodsInRsaEncryptionEncoder.rst', ], ], + 'TYPO3\CMS\Extbase\Validation\ValidatorResolver->buildMethodArgumentsValidatorConjunctions' => [ + 'numberOfMandatoryArguments' => 1, + 'maximumNumberOfArguments' => 1, + 'restFiles' => [ + 'Deprecation-83475-AggregateValidatorInformationInClassSchema-1.rst', + ], + ], ]; diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php index e156b20d1b55..cec59b78e80a 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php @@ -540,4 +540,11 @@ 'Deprecation-83254-MovedPageGenerationMethodsIntoTSFE.rst', ], ], + 'TYPO3\CMS\Extbase\Mvc\Controller\ActionController::getActionMethodParameters' => [ + 'numberOfMandatoryArguments' => 2, + 'maximumNumberOfArguments' => 4, + 'restFiles' => [ + 'Deprecation-83475-AggregateValidatorInformationInClassSchema-2.rst', + ], + ], ];