[!!!][TASK] Aggregate validator information in class schema 61/55261/13
authorAlexander Schnitzler <git@alexanderschnitzler.de>
Thu, 4 Jan 2018 10:34:42 +0000 (11:34 +0100)
committerFrank Naegler <frank.naegler@typo3.org>
Thu, 29 Mar 2018 13:37:00 +0000 (15:37 +0200)
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 <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Frank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Naegler <frank.naegler@typo3.org>
17 files changed:
typo3/sysext/core/Documentation/Changelog/master/Breaking-83475-AggregateValidatorInformationInClassSchema.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Deprecation-83475-AggregateValidatorInformationInClassSchema-1.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Deprecation-83475-AggregateValidatorInformationInClassSchema-2.rst [new file with mode: 0644]
typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php
typo3/sysext/extbase/Classes/Reflection/ClassSchema.php
typo3/sysext/extbase/Classes/Validation/ValidatorResolver.php
typo3/sysext/extbase/Tests/Unit/Reflection/ClassSchemaTest.php
typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/DummyClassWithValidateAnnotation.php [deleted file]
typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/DummyController.php [new file with mode: 0644]
typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/DummyControllerWithValidateAnnotationWithoutParam.php [new file with mode: 0644]
typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/DummyControllerWithValidateAnnotationWithoutParamTypeHint.php [new file with mode: 0644]
typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/DummyModel.php
typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/Validation/Validator/DummyValidator.php [new file with mode: 0644]
typo3/sysext/extbase/Tests/Unit/Reflection/ReflectionServiceTest.php
typo3/sysext/extbase/Tests/UnitDeprecated/Validation/ValidatorResolverTest.php [new file with mode: 0644]
typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.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 (file)
index 0000000..550ced0
--- /dev/null
@@ -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 (file)
index 0000000..a5a88b6
--- /dev/null
@@ -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 (file)
index 0000000..180d6ae
--- /dev/null
@@ -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
index d5af3a1..82854de 100644 (file)
@@ -21,6 +21,7 @@ use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
 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 @@ class ActionController extends AbstractController
      */
     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 @@ class ActionController extends AbstractController
      * @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 = [];
index 3c7f681..4eb4190 100644 (file)
@@ -17,6 +17,8 @@ namespace TYPO3\CMS\Extbase\Reflection;
 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\Lazy;
 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
@@ -90,6 +96,11 @@ class ClassSchema
     private $isSingleton;
 
     /**
+     * @var bool
+     */
+    private $isController;
+
+    /**
      * @var array
      */
     private $methods;
@@ -123,6 +134,7 @@ class ClassSchema
         $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 @@ class ClassSchema
                 'type'        => null, // Extbase
                 'elementType' => null, // Extbase
                 'annotations' => [],
-                'tags'        => []
+                'tags'        => [],
+                'validators'  => []
             ];
 
             $docCommentParser = new DocCommentParser(true);
@@ -179,10 +192,24 @@ class ClassSchema
             $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 @@ class ClassSchema
             $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 @@ class ClassSchema
                         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 @@ class ClassSchema
                     return $tag === 'validate' ? $value : ltrim($value, '$');
                 }, $values);
             }
+            unset($methodValidatorDefinition);
 
             foreach ($annotationReader->getMethodAnnotations($reflectionMethod) as $annotation) {
                 if ($annotation instanceof IgnoreValidation) {
@@ -359,6 +401,7 @@ class ClassSchema
                 $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 @@ class ClassSchema
                 ) {
                     $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
index 7e0d5a5..c55789b 100644 (file)
@@ -110,6 +110,7 @@ class ValidatorResolver implements \TYPO3\CMS\Core\SingletonInterface
 
             $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 @@ class ValidatorResolver implements \TYPO3\CMS\Core\SingletonInterface
      * @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 @@ class ValidatorResolver implements \TYPO3\CMS\Core\SingletonInterface
      *
      * @param string $validateValue
      * @return array
+     * @internal
      */
-    protected function parseValidatorAnnotation($validateValue)
+    public function parseValidatorAnnotation($validateValue)
     {
         $matches = [];
         if ($validateValue[0] === '$') {
@@ -432,8 +440,9 @@ class ValidatorResolver implements \TYPO3\CMS\Core\SingletonInterface
      *
      * @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
index 7d03fab..cb26c20 100644 (file)
@@ -17,6 +17,10 @@ namespace TYPO3\CMS\Extbase\Tests\Unit\Reflection;
 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 @@ class ClassSchemaTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
     /**
      * @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/DummyClassWithValidateAnnotation.php b/typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/DummyClassWithValidateAnnotation.php
deleted file mode 100644 (file)
index 51314b1..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-namespace TYPO3\CMS\Extbase\Tests\Unit\Reflection\Fixture;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
-
-/**
- * Fixture class with @validate annotations
- */
-class DummyClassWithValidateAnnotation extends AbstractEntity
-{
-    protected $propertyWithoutValidateAnnotations;
-
-    /**
-     * @validate NotEmpty
-     * @validate Empty (Foo=Bar)
-     */
-    protected $propertyWithValidateAnnotations;
-
-    public function methodWithoutValidateAnnotations()
-    {
-    }
-
-    /**
-     * @param $fooParam
-     * @validate $fooParam FooValidator (FooValidatorOptionKey=FooValidatorOptionValue)
-     * @validate $fooParam BarValidator
-     */
-    public function methodWithValidateAnnotations($fooParam)
-    {
-    }
-}
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 (file)
index 0000000..308bdcd
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Extbase\Tests\Unit\Reflection\Fixture;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
+
+/**
+ * Fixture class with @validate annotations
+ */
+class DummyController extends ActionController
+{
+    /**
+     * @param $fooParam
+     */
+    public function methodWithoutValidateAnnotationsAction($fooParam)
+    {
+    }
+
+    /**
+     * @param string $fooParam
+     * @validate $fooParam StringLength (minimum=1,maximum=10)
+     * @validate $fooParam NotEmpty
+     * @validate $fooParam TYPO3.CMS.Extbase:NotEmpty
+     * @validate $fooParam TYPO3.CMS.Extbase.Tests.Unit.Reflection.Fixture:DummyValidator
+     * @validate $fooParam \TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator
+     * @validate $fooParam TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator
+     */
+    public function methodWithValidateAnnotationsAction($fooParam)
+    {
+    }
+}
diff --git a/typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/DummyControllerWithValidateAnnotationWithoutParam.php b/typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/DummyControllerWithValidateAnnotationWithoutParam.php
new file mode 100644 (file)
index 0000000..6ec7c54
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Extbase\Tests\Unit\Reflection\Fixture;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
+
+/**
+ * Fixture class with @validate annotations
+ */
+class DummyControllerWithValidateAnnotationWithoutParam extends ActionController
+{
+    /**
+     * @validate $fooParam NotEmpty
+     * @validate $fooParam StringLength
+     */
+    public function methodWithValidateAnnotationsAction()
+    {
+    }
+}
diff --git a/typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/DummyControllerWithValidateAnnotationWithoutParamTypeHint.php b/typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/DummyControllerWithValidateAnnotationWithoutParamTypeHint.php
new file mode 100644 (file)
index 0000000..1631fd4
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+namespace TYPO3\CMS\Extbase\Tests\Unit\Reflection\Fixture;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
+
+/**
+ * Fixture class with @validate annotations
+ */
+class DummyControllerWithValidateAnnotationWithoutParamTypeHint extends ActionController
+{
+    /**
+     * @validate $fooParam NotEmpty
+     */
+    public function methodWithValidateAnnotationsAction($fooParam)
+    {
+    }
+}
index bb933c4..3db902b 100644 (file)
@@ -21,4 +21,15 @@ use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
  */
 class DummyModel extends AbstractEntity
 {
+    protected $propertyWithoutValidateAnnotations;
+
+    /**
+     * @validate StringLength (minimum=1,maximum=10)
+     * @validate NotEmpty
+     * @validate TYPO3.CMS.Extbase:NotEmpty
+     * @validate TYPO3.CMS.Extbase.Tests.Unit.Reflection.Fixture:DummyValidator
+     * @validate \TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator
+     * @validate TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator
+     */
+    protected $propertyWithValidateAnnotations;
 }
diff --git a/typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/Validation/Validator/DummyValidator.php b/typo3/sysext/extbase/Tests/Unit/Reflection/Fixture/Validation/Validator/DummyValidator.php
new file mode 100644 (file)
index 0000000..c2215f0
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Extbase\Tests\Unit\Reflection\Fixture\Validation\Validator;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
+
+/**
+ * Fixture validator
+ */
+class DummyValidator implements ValidatorInterface
+{
+    /**
+     * @param mixed $value The value that should be validated
+     * @return \TYPO3\CMS\Extbase\Error\Result
+     */
+    public function validate($value)
+    {
+        return new \TYPO3\CMS\Extbase\Error\Result;
+    }
+
+    /**
+     * @return array
+     */
+    public function getOptions()
+    {
+        return [];
+    }
+}
index 33a8e13..310e53f 100644 (file)
@@ -140,6 +140,7 @@ class ReflectionServiceTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCa
                 'hasDefaultValue' =>  false,
                 'defaultValue' =>  null,
                 'dependency' =>  null,
+                'validators' => [],
             ]
         ], $parameters);
 
@@ -175,6 +176,7 @@ class ReflectionServiceTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCa
                 'hasDefaultValue' =>  false,
                 'defaultValue' =>  null,
                 'dependency' =>  null,
+                'validators' => [],
             ],
             'foo' => [
                 'position' => 1,
@@ -189,6 +191,7 @@ class ReflectionServiceTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCa
                 '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 (file)
index 0000000..fd31d32
--- /dev/null
@@ -0,0 +1,137 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Extbase\Tests\UnitDeprecated\Validation;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationConfigurationException;
+
+/**
+ * Test case
+ */
+class ValidatorResolverTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
+{
+
+    /**
+     * @test
+     */
+    public function buildMethodArgumentsValidatorConjunctionsReturnsEmptyArrayIfMethodHasNoArguments()
+    {
+        $mockController = $this->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');
+    }
+}
index 405cce2..94a0a74 100644 (file)
@@ -2025,4 +2025,11 @@ return [
             'Deprecation-84407-AJAXRequestMethodsInRsaEncryptionEncoder.rst',
         ],
     ],
+    'TYPO3\CMS\Extbase\Validation\ValidatorResolver->buildMethodArgumentsValidatorConjunctions' => [
+        'numberOfMandatoryArguments' => 1,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-83475-AggregateValidatorInformationInClassSchema-1.rst',
+        ],
+    ],
 ];
index e156b20..cec59b7 100644 (file)
@@ -540,4 +540,11 @@ return [
             'Deprecation-83254-MovedPageGenerationMethodsIntoTSFE.rst',
         ],
     ],
+    'TYPO3\CMS\Extbase\Mvc\Controller\ActionController::getActionMethodParameters' => [
+        'numberOfMandatoryArguments' => 2,
+        'maximumNumberOfArguments' => 4,
+        'restFiles' => [
+            'Deprecation-83475-AggregateValidatorInformationInClassSchema-2.rst',
+        ],
+    ],
 ];