TYPO3 API  SVNRelease
ValidatorResolver.php
Go to the documentation of this file.
00001 <?php
00002 /***************************************************************
00003 *  Copyright notice
00004 *
00005 *  (c) 2009 Jochen Rau <jochen.rau@typoplanet.de>
00006 *  All rights reserved
00007 *
00008 *  This class is a backport of the corresponding class of FLOW3.
00009 *  All credits go to the v5 team.
00010 *
00011 *  This script is part of the TYPO3 project. The TYPO3 project is
00012 *  free software; you can redistribute it and/or modify
00013 *  it under the terms of the GNU General Public License as published by
00014 *  the Free Software Foundation; either version 2 of the License, or
00015 *  (at your option) any later version.
00016 *
00017 *  The GNU General Public License can be found at
00018 *  http://www.gnu.org/copyleft/gpl.html.
00019 *
00020 *  This script is distributed in the hope that it will be useful,
00021 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
00022 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00023 *  GNU General Public License for more details.
00024 *
00025 *  This copyright notice MUST APPEAR in all copies of the script!
00026 ***************************************************************/
00027 
00028 /**
00029  * Validator resolver to automatically find a appropriate validator for a given subject
00030  *
00031  * @package Extbase
00032  * @subpackage Validation
00033  * @version $Id: ValidatorResolver.php 1790 2010-01-18 22:27:37Z jocrau $
00034  */
00035 class Tx_Extbase_Validation_ValidatorResolver implements t3lib_Singleton {
00036 
00037     /**
00038      * Match validator names and options
00039      * @var string
00040      */
00041     const PATTERN_MATCH_VALIDATORS = '/
00042             (?:^|,\s*)
00043             (?P<validatorName>[a-z0-9_]+)
00044             \s*
00045             (?:\(
00046                 (?P<validatorOptions>(?:\s*[a-z0-9]+\s*=\s*(?:
00047                     "(?:\\\\"|[^"])*"
00048                     |\'(?:\\\\\'|[^\'])*\'
00049                     |(?:\s|[^,"\']*)
00050                 )(?:\s|,)*)*)
00051             \))?
00052         /ixS';
00053 
00054     /**
00055      * Match validator options (to parse actual options)
00056      * @var string
00057      */
00058     const PATTERN_MATCH_VALIDATOROPTIONS = '/
00059             \s*
00060             (?P<optionName>[a-z0-9]+)
00061             \s*=\s*
00062             (?P<optionValue>
00063                 "(?:\\\\"|[^"])*"
00064                 |\'(?:\\\\\'|[^\'])*\'
00065                 |(?:\s|[^,"\']*)
00066             )
00067         /ixS';
00068 
00069     /**
00070      * @var Tx_Extbase_Object_ObjectManagerInterface
00071      */
00072     protected $objectManager;
00073 
00074     /**
00075      * @var Tx_Extbase_Reflection_Service
00076      */
00077     protected $reflectionService;
00078 
00079     /**
00080      * @var array
00081      */
00082     protected $baseValidatorConjunctions = array();
00083 
00084     /**
00085      * Injects the object manager
00086      *
00087      * @param Tx_Extbase_Object_ObjectManagerInterface $objectManager A reference to the object manager
00088      * @return void
00089      */
00090     public function injectObjectManager(Tx_Extbase_Object_ObjectManagerInterface $objectManager) {
00091         $this->objectManager = $objectManager;
00092     }
00093 
00094     /**
00095      * Injects the reflection service
00096      *
00097      * @param Tx_Extbase_Reflection_Service $reflectionService
00098      * @return void
00099      */
00100     public function injectReflectionService(Tx_Extbase_Reflection_Service $reflectionService) {
00101         $this->reflectionService = $reflectionService;
00102     }
00103 
00104     /**
00105      * Get a validator for a given data type. Returns a validator implementing
00106      * the Tx_Extbase_Validation_Validator_ValidatorInterface or NULL if no validator
00107      * could be resolved.
00108      *
00109      * @param string $validatorName Either one of the built-in data types or fully qualified validator class name
00110      * @param array $validatorOptions Options to be passed to the validator
00111      * @return Tx_Extbase_Validation_Validator_ValidatorInterface Validator or NULL if none found.
00112      */
00113     public function createValidator($validatorName, array $validatorOptions = array()) {
00114         $validatorClassName = $this->resolveValidatorObjectName($validatorName);
00115         if ($validatorClassName === FALSE) return NULL;
00116         $validator = $this->objectManager->get($validatorClassName);
00117         if (!($validator instanceof Tx_Extbase_Validation_Validator_ValidatorInterface)) {
00118             return NULL;
00119         }
00120 
00121         $validator->setOptions($validatorOptions);
00122         return $validator;
00123     }
00124 
00125     /**
00126      * Resolves and returns the base validator conjunction for the given data type.
00127      *
00128      * If no validator could be resolved (which usually means that no validation is necessary),
00129      * NULL is returned.
00130      *
00131      * @param string $dataType The data type to search a validator for. Usually the fully qualified object name
00132      * @return Tx_Extbase_Validation_Validator_ConjunctionValidator The validator conjunction or NULL
00133      */
00134     public function getBaseValidatorConjunction($dataType) {
00135         if (!isset($this->baseValidatorConjunctions[$dataType])) {
00136             $this->baseValidatorConjunctions[$dataType] = $this->buildBaseValidatorConjunction($dataType);
00137         }
00138         return $this->baseValidatorConjunctions[$dataType];
00139     }
00140 
00141     /**
00142      * Detects and registers any validators for arguments:
00143      * - by the data type specified in the @param annotations
00144      * - additional validators specified in the @validate annotations of a method
00145      *
00146      * @return array An Array of ValidatorConjunctions for each method parameters.
00147      */
00148     public function buildMethodArgumentsValidatorConjunctions($className, $methodName) {
00149         $validatorConjunctions = array();
00150 
00151         $methodParameters = $this->reflectionService->getMethodParameters($className, $methodName);
00152         $methodTagsValues = $this->reflectionService->getMethodTagsValues($className, $methodName);
00153         if (!count($methodParameters)) {
00154             // early return in case no parameters were found.
00155             return $validatorConjunctions;
00156         }
00157         foreach ($methodParameters as $parameterName => $methodParameter) {
00158             $validatorConjunction = $this->createValidator('Conjunction');
00159             $typeValidator = $this->createValidator($methodParameter['type']);
00160             if ($typeValidator !== NULL) $validatorConjunction->addValidator($typeValidator);
00161             $validatorConjunctions[$parameterName] = $validatorConjunction;
00162         }
00163 
00164         if (isset($methodTagsValues['validate'])) {
00165             foreach ($methodTagsValues['validate'] as $validateValue) {
00166                 $parsedAnnotation = $this->parseValidatorAnnotation($validateValue);
00167                 foreach ($parsedAnnotation['validators'] as $validatorConfiguration) {
00168                     $newValidator = $this->createValidator($validatorConfiguration['validatorName'], $validatorConfiguration['validatorOptions']);
00169                     if ($newValidator === NULL) throw new Tx_Extbase_Validation_Exception_NoSuchValidator('Invalid validate annotation in ' . $className . '->' . $methodName . '(): Could not resolve class name for  validator "' . $validatorConfiguration['validatorName'] . '".', 1239853109);
00170 
00171                     if  (isset($validatorConjunctions[$parsedAnnotation['argumentName']])) {
00172                         $validatorConjunctions[$parsedAnnotation['argumentName']]->addValidator($newValidator);
00173                     } else {
00174                         throw new Tx_Extbase_Validation_Exception_InvalidValidationConfiguration('Invalid validate annotation in ' . $className . '->' . $methodName . '(): Validator specified for argument name "' . $parsedAnnotation['argumentName'] . '", but this argument does not exist.', 1253172726);
00175                     }
00176                 }
00177             }
00178         }
00179         return $validatorConjunctions;
00180     }
00181 
00182     /**
00183      * Builds a base validator conjunction for the given data type.
00184      *
00185      * The base validation rules are those which were declared directly in a class (typically
00186      * a model) through some @validate annotations on properties.
00187      *
00188      * Additionally, if a custom validator was defined for the class in question, it will be added
00189      * to the end of the conjunction. A custom validator is found if it follows the naming convention
00190      * "Replace '\Model\' by '\Validator\' and append "Validator".
00191      *
00192      * Example: $dataType is F3\Foo\Domain\Model\Quux, then the Validator will be found if it has the
00193      * name F3\Foo\Domain\Validator\QuuxValidator
00194      *
00195      * @param string $dataType The data type to build the validation conjunction for. Needs to be the fully qualified object name.
00196      * @return Tx_Extbase_Validation_Validator_ConjunctionValidator The validator conjunction or NULL
00197      */
00198     protected function buildBaseValidatorConjunction($dataType) {
00199         $validatorConjunction = $this->objectManager->get('Tx_Extbase_Validation_Validator_ConjunctionValidator');
00200 
00201         // Model based validator
00202         if (strstr($dataType, '_') !== FALSE && class_exists($dataType)) {
00203             $validatorCount = 0;
00204             $objectValidator = $this->createValidator('GenericObject');
00205 
00206             foreach ($this->reflectionService->getClassPropertyNames($dataType) as $classPropertyName) {
00207                 $classPropertyTagsValues = $this->reflectionService->getPropertyTagsValues($dataType, $classPropertyName);
00208                 if (!isset($classPropertyTagsValues['validate'])) continue;
00209 
00210                 foreach ($classPropertyTagsValues['validate'] as $validateValue) {
00211                     $parsedAnnotation = $this->parseValidatorAnnotation($validateValue);
00212                     foreach ($parsedAnnotation['validators'] as $validatorConfiguration) {
00213                         $newValidator = $this->createValidator($validatorConfiguration['validatorName'], $validatorConfiguration['validatorOptions']);
00214                         if ($newValidator === NULL) {
00215                             throw new Tx_Extbase_Validation_Exception_NoSuchValidator('Invalid validate annotation in ' . $dataType . '::' . $classPropertyName . ': Could not resolve class name for  validator "' . $validatorConfiguration['validatorName'] . '".', 1241098027);
00216                         }
00217                         $objectValidator->addPropertyValidator($classPropertyName, $newValidator);
00218                         $validatorCount ++;
00219                     }
00220                 }
00221             }
00222             if ($validatorCount > 0) $validatorConjunction->addValidator($objectValidator);
00223         }
00224 
00225         // Custom validator for the class
00226         $possibleValidatorClassName = str_replace('_Model_', '_Validator_', $dataType) . 'Validator';
00227         $customValidator = $this->createValidator($possibleValidatorClassName);
00228         if ($customValidator !== NULL) {
00229             $validatorConjunction->addValidator($customValidator);
00230         }
00231 
00232         return $validatorConjunction;
00233     }
00234 
00235     /**
00236      * Parses the validator options given in @validate annotations.
00237      *
00238      * @return array
00239      */
00240     protected function parseValidatorAnnotation($validateValue) {
00241         $matches = array();
00242         if ($validateValue[0] === '$') {
00243             $parts = explode(' ', $validateValue, 2);
00244             $validatorConfiguration = array('argumentName' => ltrim($parts[0], '$'), 'validators' => array());
00245             preg_match_all(self::PATTERN_MATCH_VALIDATORS, $parts[1], $matches, PREG_SET_ORDER);
00246         } else {
00247             $validatorConfiguration = array('validators' => array());
00248             preg_match_all(self::PATTERN_MATCH_VALIDATORS, $validateValue, $matches, PREG_SET_ORDER);
00249         }
00250 
00251         foreach ($matches as $match) {
00252             $validatorOptions = array();
00253             if (isset($match['validatorOptions'])) {
00254                 $validatorOptions = $this->parseValidatorOptions($match['validatorOptions']);
00255             }
00256             $validatorConfiguration['validators'][] = array('validatorName' => $match['validatorName'], 'validatorOptions' => $validatorOptions);
00257         }
00258 
00259         return $validatorConfiguration;
00260     }
00261 
00262     /**
00263      * Parses $rawValidatorOptions not containing quoted option values.
00264      * $rawValidatorOptions will be an empty string afterwards (pass by ref!).
00265      *
00266      * @param string &$rawValidatorOptions
00267      * @return array An array of optionName/optionValue pairs
00268      */
00269     protected function parseValidatorOptions($rawValidatorOptions) {
00270         $validatorOptions = array();
00271         $parsedValidatorOptions = array();
00272         preg_match_all(self::PATTERN_MATCH_VALIDATOROPTIONS, $rawValidatorOptions, $validatorOptions, PREG_SET_ORDER);
00273         foreach ($validatorOptions as $validatorOption) {
00274             $parsedValidatorOptions[trim($validatorOption['optionName'])] = trim($validatorOption['optionValue']);
00275         }
00276         array_walk($parsedValidatorOptions, array($this, 'unquoteString'));
00277         return $parsedValidatorOptions;
00278     }
00279 
00280     /**
00281      * Removes escapings from a given argument string and trims the outermost
00282      * quotes.
00283      *
00284      * This method is meant as a helper for regular expression results.
00285      *
00286      * @param string &$quotedValue Value to unquote
00287      */
00288     protected function unquoteString(&$quotedValue) {
00289         switch ($quotedValue[0]) {
00290             case '"':
00291                 $quotedValue = str_replace('\"', '"', trim($quotedValue, '"'));
00292             break;
00293             case '\'':
00294                 $quotedValue = str_replace('\\\'', '\'', trim($quotedValue, '\''));
00295             break;
00296         }
00297         $quotedValue = str_replace('\\\\', '\\', $quotedValue);
00298     }
00299 
00300     /**
00301      *
00302      *
00303      * Returns an object of an appropriate validator for the given class. If no validator is available
00304      * FALSE is returned
00305      *
00306      * @param string $validatorName Either the fully qualified class name of the validator or the short name of a built-in validator
00307      * @return string Name of the validator object or FALSE
00308      */
00309     protected function resolveValidatorObjectName($validatorName) {
00310         if (strstr($validatorName, '_') !== FALSE && class_exists($validatorName)) return $validatorName;
00311 
00312         $possibleClassName = 'Tx_Extbase_Validation_Validator_' . $this->unifyDataType($validatorName) . 'Validator';
00313         if (class_exists($possibleClassName)) return $possibleClassName;
00314 
00315         return FALSE;
00316     }
00317 
00318     /**
00319      * Preprocess data types. Used to map primitive PHP types to DataTypes used in Extbase.
00320      *
00321      * @param string $type Data type to unify
00322      * @return string unified data type
00323      */
00324     protected function unifyDataType($type) {
00325         switch ($type) {
00326             case 'int' :
00327                 $type = 'Integer';
00328                 break;
00329             case 'bool' :
00330                 $type = 'Boolean';
00331                 break;
00332             case 'double' :
00333                 $type = 'Float';
00334                 break;
00335             case 'numeric' :
00336                 $type = 'Number';
00337                 break;
00338             case 'mixed' :
00339                 $type = 'Raw';
00340                 break;
00341         }
00342         return ucfirst($type);
00343     }
00344 
00345 }
00346 
00347 ?>