Skip to content

Commit

Permalink
[!!!][TASK] Aggregate validator information in class schema
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
alexanderschnitzler authored and NeoBlack committed Mar 29, 2018
1 parent b9026a9 commit 84879ed
Show file tree
Hide file tree
Showing 16 changed files with 639 additions and 52 deletions.
@@ -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
@@ -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
@@ -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
48 changes: 33 additions & 15 deletions typo3/sysext/extbase/Classes/Mvc/Controller/ActionController.php
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 = [];
Expand Down
80 changes: 73 additions & 7 deletions typo3/sysext/extbase/Classes/Reflection/ClassSchema.php
Expand Up @@ -17,14 +17,20 @@
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;
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
Expand Down Expand Up @@ -89,6 +95,11 @@ class ClassSchema
*/
private $isSingleton;

/**
* @var bool
*/
private $isController;

/**
* @var array
*/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -164,7 +176,8 @@ protected function reflectProperties(\ReflectionClass $reflectionClass)
'type' => null, // Extbase
'elementType' => null, // Extbase
'annotations' => [],
'tags' => []
'tags' => [],
'validators' => []
];

$docCommentParser = new DocCommentParser(true);
Expand All @@ -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) {
Expand Down Expand Up @@ -307,21 +334,35 @@ 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(
'Tagging methods with @ignorevalidation is deprecated and will be removed in TYPO3 v10.0.',
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
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 84879ed

Please sign in to comment.