Moving all validation logic into a new ModelValidator object.

This commit is contained in:
Thomas Ploch 2012-03-22 12:18:57 +01:00 committed by Ceeram
parent 061483d03e
commit 324684c14f
8 changed files with 1259 additions and 311 deletions

View file

@ -528,7 +528,7 @@ class Controller extends Object implements CakeEventListener {
}
/**
* Merge components, helpers, and uses vars from
* Merge components, helpers, and uses vars from
* Controller::$_mergeParent and PluginAppController.
*
* @return void

View file

@ -25,6 +25,7 @@ App::uses('String', 'Utility');
App::uses('Hash', 'Utility');
App::uses('BehaviorCollection', 'Model');
App::uses('ModelBehavior', 'Model');
App::uses('ModelValidator', 'Model');
App::uses('ConnectionManager', 'Model');
App::uses('Xml', 'Utility');
App::uses('CakeEvent', 'Event');
@ -618,6 +619,13 @@ class Model extends Object implements CakeEventListener {
*/
protected $_eventManager = null;
/**
* Instance of the ModelValidator
*
* @var ModelValidator
*/
protected $_validator = null;
/**
* Constructor. Binds the model's database table to the object.
*
@ -723,6 +731,7 @@ class Model extends Object implements CakeEventListener {
$this->_createLinks();
$this->Behaviors->init($this->alias, $this->actsAs);
$this->setValidator();
}
/**
@ -969,9 +978,8 @@ class Model extends Object implements CakeEventListener {
$value = array();
if (strpos($assoc, '.') !== false) {
list($plugin, $assoc) = pluginSplit($assoc);
$this->{$type}[$assoc] = array('className' => $plugin . '.' . $assoc);
} else {
list($plugin, $assoc) = pluginSplit($assoc, true);
$this->{$type}[$assoc] = array('className' => $plugin . $assoc); } else {
$this->{$type}[$assoc] = $value;
}
}
@ -1445,7 +1453,7 @@ class Model extends Object implements CakeEventListener {
$defaults = array();
$this->id = false;
$this->data = array();
$this->validationErrors = array();
$this->validationErrors = $this->getValidator()->validationErrors = array();
if ($data !== null && $data !== false) {
foreach ($this->schema() as $field => $properties) {
@ -2032,7 +2040,7 @@ class Model extends Object implements CakeEventListener {
}
$options = array_merge(array('validate' => 'first', 'atomic' => true, 'deep' => false), $options);
$this->validationErrors = $validationErrors = array();
$validationErrors = array();
if (empty($data) && $options['validate'] !== false) {
$result = $this->save($data, $options);
@ -2108,30 +2116,7 @@ class Model extends Object implements CakeEventListener {
* depending on whether each record validated successfully.
*/
public function validateMany($data, $options = array()) {
$options = array_merge(array('atomic' => true, 'deep' => false), $options);
$this->validationErrors = $validationErrors = $return = array();
foreach ($data as $key => $record) {
if ($options['deep']) {
$validates = $this->validateAssociated($record, $options);
} else {
$validates = $this->create($record) && $this->validates($options);
}
if ($validates === false || (is_array($validates) && in_array(false, $validates, true))) {
$validationErrors[$key] = $this->validationErrors;
$validates = false;
} else {
$validates = true;
}
$return[$key] = $validates;
}
$this->validationErrors = $validationErrors;
if (!$options['atomic']) {
return $return;
}
if (empty($this->validationErrors)) {
return true;
}
return false;
return $this->getValidator()->validateMany($data, $options);
}
/**
@ -2166,7 +2151,7 @@ class Model extends Object implements CakeEventListener {
}
$options = array_merge(array('validate' => 'first', 'atomic' => true, 'deep' => false), $options);
$this->validationErrors = $validationErrors = array();
$validationErrors = array();
if (empty($data) && $options['validate'] !== false) {
$result = $this->save($data, $options);
@ -2306,53 +2291,7 @@ class Model extends Object implements CakeEventListener {
* depending on whether each record validated successfully.
*/
public function validateAssociated($data, $options = array()) {
$options = array_merge(array('atomic' => true, 'deep' => false), $options);
$this->validationErrors = $validationErrors = $return = array();
if (!($this->create($data) && $this->validates($options))) {
$validationErrors[$this->alias] = $this->validationErrors;
$return[$this->alias] = false;
} else {
$return[$this->alias] = true;
}
$associations = $this->getAssociated();
foreach ($data as $association => $values) {
$validates = true;
if (isset($associations[$association])) {
if (in_array($associations[$association], array('belongsTo', 'hasOne'))) {
if ($options['deep']) {
$validates = $this->{$association}->validateAssociated($values, $options);
} else {
$validates = $this->{$association}->create($values) !== null && $this->{$association}->validates($options);
}
if (is_array($validates)) {
if (in_array(false, $validates, true)) {
$validates = false;
} else {
$validates = true;
}
}
$return[$association] = $validates;
} elseif ($associations[$association] === 'hasMany') {
$validates = $this->{$association}->validateMany($values, $options);
$return[$association] = $validates;
}
if (!$validates || (is_array($validates) && in_array(false, $validates, true))) {
$validationErrors[$association] = $this->{$association}->validationErrors;
}
}
}
$this->validationErrors = $validationErrors;
if (isset($validationErrors[$this->alias])) {
$this->validationErrors = $validationErrors[$this->alias];
}
if (!$options['atomic']) {
return $return;
}
if ($return[$this->alias] === false || !empty($this->validationErrors)) {
return false;
}
return true;
return $this->getValidator()->validateAssociated($data, $options);
}
/**
@ -3039,14 +2978,7 @@ class Model extends Object implements CakeEventListener {
* @return boolean True if there are no errors
*/
public function validates($options = array()) {
$errors = $this->invalidFields($options);
if (empty($errors) && $errors !== false) {
$errors = $this->_validateWithModels($options);
}
if (is_array($errors)) {
return count($errors) === 0;
}
return $errors;
return $this->getValidator()->validates($options);
}
/**
@ -3057,203 +2989,7 @@ class Model extends Object implements CakeEventListener {
* @see Model::validates()
*/
public function invalidFields($options = array()) {
$event = new CakeEvent('Model.beforeValidate', $this, array($options));
list($event->break, $event->breakOn) = array(true, false);
$this->getEventManager()->dispatch($event);
if ($event->isStopped()) {
return false;
}
if (!isset($this->validate) || empty($this->validate)) {
return $this->validationErrors;
}
$data = $this->data;
$methods = array_map('strtolower', get_class_methods($this));
$behaviorMethods = array_keys($this->Behaviors->methods());
if (isset($data[$this->alias])) {
$data = $data[$this->alias];
} elseif (!is_array($data)) {
$data = array();
}
$exists = null;
$_validate = $this->validate;
$whitelist = $this->whitelist;
if (!empty($options['fieldList'])) {
if (!empty($options['fieldList'][$this->alias]) && is_array($options['fieldList'][$this->alias])) {
$whitelist = $options['fieldList'][$this->alias];
} else {
$whitelist = $options['fieldList'];
}
}
if (!empty($whitelist)) {
$validate = array();
foreach ((array)$whitelist as $f) {
if (!empty($this->validate[$f])) {
$validate[$f] = $this->validate[$f];
}
}
$this->validate = $validate;
}
$validationDomain = $this->validationDomain;
if (empty($validationDomain)) {
$validationDomain = 'default';
}
foreach ($this->validate as $fieldName => $ruleSet) {
if (!is_array($ruleSet) || (is_array($ruleSet) && isset($ruleSet['rule']))) {
$ruleSet = array($ruleSet);
}
$default = array(
'allowEmpty' => null,
'required' => null,
'rule' => 'blank',
'last' => true,
'on' => null
);
foreach ($ruleSet as $index => $validator) {
if (!is_array($validator)) {
$validator = array('rule' => $validator);
}
$validator = array_merge($default, $validator);
if (!empty($validator['on']) || in_array($validator['required'], array('create', 'update'), true)) {
if ($exists === null) {
$exists = $this->exists();
}
if ($validator['on'] == 'create' && $exists || $validator['on'] == 'update' && !$exists) {
continue;
}
if ($validator['required'] === 'create' && !$exists || $validator['required'] === 'update' && $exists) {
$validator['required'] = true;
}
}
$valid = true;
$requiredFail = (
(!isset($data[$fieldName]) && $validator['required'] === true) ||
(
isset($data[$fieldName]) && (empty($data[$fieldName]) &&
!is_numeric($data[$fieldName])) && $validator['allowEmpty'] === false
)
);
if (!$requiredFail && array_key_exists($fieldName, $data)) {
if (empty($data[$fieldName]) && $data[$fieldName] != '0' && $validator['allowEmpty'] === true) {
break;
}
if (is_array($validator['rule'])) {
$rule = $validator['rule'][0];
unset($validator['rule'][0]);
$ruleParams = array_merge(array($data[$fieldName]), array_values($validator['rule']));
} else {
$rule = $validator['rule'];
$ruleParams = array($data[$fieldName]);
}
if (in_array(strtolower($rule), $methods)) {
$ruleParams[] = $validator;
$ruleParams[0] = array($fieldName => $ruleParams[0]);
$valid = $this->dispatchMethod($rule, $ruleParams);
} elseif (in_array($rule, $behaviorMethods) || in_array(strtolower($rule), $behaviorMethods)) {
$ruleParams[] = $validator;
$ruleParams[0] = array($fieldName => $ruleParams[0]);
$valid = $this->Behaviors->dispatchMethod($this, $rule, $ruleParams);
} elseif (method_exists('Validation', $rule)) {
$valid = call_user_func_array(array('Validation', $rule), $ruleParams);
} elseif (!is_array($validator['rule'])) {
$valid = preg_match($rule, $data[$fieldName]);
} elseif (Configure::read('debug') > 0) {
trigger_error(__d('cake_dev', 'Could not find validation handler %s for %s', $rule, $fieldName), E_USER_WARNING);
}
}
if ($requiredFail || !$valid || (is_string($valid) && strlen($valid) > 0)) {
if (is_string($valid)) {
$message = $valid;
} elseif (isset($validator['message'])) {
$args = null;
if (is_array($validator['message'])) {
$message = $validator['message'][0];
$args = array_slice($validator['message'], 1);
} else {
$message = $validator['message'];
}
if (is_array($validator['rule']) && $args === null) {
$args = array_slice($ruleSet[$index]['rule'], 1);
}
if (!empty($args)) {
foreach ($args as $k => $arg) {
$args[$k] = __d($validationDomain, $arg);
}
}
$message = __d($validationDomain, $message, $args);
} elseif (is_string($index)) {
if (is_array($validator['rule'])) {
$args = array_slice($ruleSet[$index]['rule'], 1);
$message = __d($validationDomain, $index, $args);
} else {
$message = __d($validationDomain, $index);
}
} elseif (!$requiredFail && is_numeric($index) && count($ruleSet) > 1) {
$message = $index + 1;
} else {
$message = __d('cake_dev', 'This field cannot be left blank');
}
$this->invalidate($fieldName, $message);
if ($validator['last']) {
break;
}
}
}
}
$this->validate = $_validate;
return $this->validationErrors;
}
/**
* Runs validation for hasAndBelongsToMany associations that have 'with' keys
* set. And data in the set() data set.
*
* @param array $options Array of options to use on Validation of with models
* @return boolean Failure of validation on with models.
* @see Model::validates()
*/
protected function _validateWithModels($options) {
$valid = true;
foreach ($this->hasAndBelongsToMany as $assoc => $association) {
if (empty($association['with']) || !isset($this->data[$assoc])) {
continue;
}
list($join) = $this->joinModel($this->hasAndBelongsToMany[$assoc]['with']);
$data = $this->data[$assoc];
$newData = array();
foreach ((array)$data as $row) {
if (isset($row[$this->hasAndBelongsToMany[$assoc]['associationForeignKey']])) {
$newData[] = $row;
} elseif (isset($row[$join]) && isset($row[$join][$this->hasAndBelongsToMany[$assoc]['associationForeignKey']])) {
$newData[] = $row[$join];
}
}
if (empty($newData)) {
continue;
}
foreach ($newData as $data) {
$data[$this->hasAndBelongsToMany[$assoc]['foreignKey']] = $this->id;
$this->{$join}->create($data);
$valid = ($valid && $this->{$join}->validates($options));
}
}
return $valid;
return $this->getValidator()->invalidFields($options);
}
/**
@ -3266,10 +3002,7 @@ class Model extends Object implements CakeEventListener {
* @return void
*/
public function invalidate($field, $value = true) {
if (!is_array($this->validationErrors)) {
$this->validationErrors = array();
}
$this->validationErrors[$field][] = $value;
$this->getValidator()->invalidate($field, $value);
}
/**
@ -3616,4 +3349,78 @@ class Model extends Object implements CakeEventListener {
}
}
/**
* Creates a ModelValidator instance from Model::validatorClass
*
* @return void
* @throws MissingValidatorException
* @throws InvalidValidatorException
*/
public function setValidator($validator = null) {
if (is_object($validator) && $this->_isValidValidator($validator)) {
$this->_validator = $validator;
return $this;
}
if (is_null($validator) && is_null($this->validatorClass)) {
$this->validatorClass = ModelValidator::DEFAULT_VALIDATOR;
} elseif (is_string($validator)) {
$this->validatorClass = $validator;
}
if (!$this->_loadValidator($this->validatorClass)) {
throw new MissingValidatorException(array($this->validatorClass));
}
if (!$this->_isValidValidator($this->_validator)) {
$this->_validator = null;
throw new InvalidValidatorException(array($this->validatorClass, ModelValidator::DEFAULT_VALIDATOR));
}
return $this;
}
/**
* Returns the currently set ModelValidator instance
*
* @return ModelValidator
*/
public function getValidator() {
return $this->_validator;
}
/**
* Tries to load a validator and returns true if the class could be found, false otherwise.
*
* @param string $validatorClass The class to be loaded
* @return boolean True if the class was found, false otherwise
*/
protected function _loadValidator($validatorClass) {
list($plugin, $class) = pluginSplit($validatorClass, true);
unset($validatorClass);
$location = $plugin . 'Model';
App::uses($class, $location);
if (!class_exists($class, true)) {
return false;
}
$this->_validator = new $class($this);
return true;
}
/**
* Checks if the passed in validator instance is either an instance or subclass of ModelValidator.
*
* @param $validator
* @return boolean True if the instance is valid, false otherwise
*/
protected function _isValidValidator($validator) {
if (!($validator instanceof ModelValidator) && !is_subclass_of($validator, ModelValidator::DEFAULT_VALIDATOR)) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,543 @@
<?php
/**
* ModelValidator.
*
* Provides the Model validation logic.
*
* PHP versions 5
*
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @package Cake.Model
* @since CakePHP(tm) v 0.10.0.0
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
App::uses('CakeField', 'Model/Validator');
App::uses('CakeRule', 'Model/Validator');
/**
* ModelValidator object.
*
* @package Cake.Model
* @link http://book.cakephp.org/2.0/en/data-validation.html
*/
class ModelValidator {
/**
* The default ModelValidator class name
*
* @var string
*/
const DEFAULT_VALIDATOR = 'ModelValidator';
/**
* The default validation domain
*
* @var string
*/
const DEFAULT_DOMAIN = 'default';
/**
* Holds the data array from the Model
*
* @var array
*/
public $data = array();
/**
* The default ValidationDomain
*
* @var string
*/
public $validationDomain = 'default';
/**
* Holds the validationErrors
*
* @var array
*/
public $validationErrors = array();
/**
* Holds the options
*
* @var array
*/
public $options = array();
/**
* Holds the ModelFields
*
* @var array
*/
protected $_fields = array();
/**
* Holds the reference to the model the Validator is attached to
*
* @var Model
*/
protected $_model = array();
/**
* The validators $validate property
*
* @var array
*/
protected $_validate = array();
/**
* Holds the available custom callback methods
*
* @var array
*/
protected $_methods = array();
/**
* Constructor
*
* @param Model $Model A reference to the Model the Validator is attached to
*/
public function __construct(Model $Model) {
$this->_model = $Model;
}
/**
* Returns true if all fields pass validation. Will validate hasAndBelongsToMany associations
* that use the 'with' key as well. Since _saveMulti is incapable of exiting a save operation.
*
* Will validate the currently set data. Use Model::set() or Model::create() to set the active data.
*
* @param array $options An optional array of custom options to be made available in the beforeValidate callback
* @return boolean True if there are no errors
*/
public function validates($options = array()) {
$this->validationErrors = array();
$errors = $this->invalidFields($options);
if (empty($errors) && $errors !== false) {
$errors = $this->_validateWithModels($options);
}
if (is_array($errors)) {
return count($errors) === 0;
}
return $errors;
}
/**
* Validates a single record, as well as all its directly associated records.
*
* #### Options
*
* - atomic: If true (default), returns boolean. If false returns array.
* - fieldList: Equivalent to the $fieldList parameter in Model::save()
* - deep: If set to true, not only directly associated data , but deeper nested associated data is validated as well.
*
* @param array $data Record data to validate. This should be an array indexed by association name.
* @param array $options Options to use when validating record data (see above), See also $options of validates().
* @return array|boolean If atomic: True on success, or false on failure.
* Otherwise: array similar to the $data array passed, but values are set to true/false
* depending on whether each record validated successfully.
*/
public function validateAssociated($data, $options = array()) {
$options = array_merge(array('atomic' => true, 'deep' => false), $options);
$this->validationErrors = $this->getModel()->validationErrors = $return = array();
if (!($this->getModel()->create($data) && $this->validates($options))) {
$this->validationErrors = array($this->getModel()->alias => $this->validationErrors);
$return[$this->getModel()->alias] = false;
} else {
$return[$this->getModel()->alias] = true;
}
$associations = $this->getModel()->getAssociated();
foreach ($data as $association => $values) {
$validates = true;
if (isset($associations[$association])) {
if (in_array($associations[$association], array('belongsTo', 'hasOne'))) {
if ($options['deep']) {
$validates = $this->getModel()->{$association}->getValidator()->validateAssociated($values, $options);
} else {
$validates = $this->getModel()->{$association}->create($values) !== null && $this->getModel()->{$association}->getValidator()->validates($options);
}
if (is_array($validates)) {
if (in_array(false, $validates, true)) {
$validates = false;
} else {
$validates = true;
}
}
$return[$association] = $validates;
} elseif ($associations[$association] === 'hasMany') {
$validates = $this->getModel()->{$association}->getValidator()->validateMany($values, $options);
$return[$association] = $validates;
}
if (!$validates || (is_array($validates) && in_array(false, $validates, true))) {
$this->validationErrors[$association] = $this->getModel()->{$association}->getValidator()->validationErrors;
}
}
}
if (isset($this->validationErrors[$this->getModel()->alias])) {
$this->validationErrors = $this->validationErrors[$this->getModel()->alias];
}
$this->getModel()->validationErrors = $this->validationErrors;
if (!$options['atomic']) {
return $return;
}
if ($return[$this->getModel()->alias] === false || !empty($this->validationErrors)) {
return false;
}
return true;
}
/**
* Validates multiple individual records for a single model
*
* #### Options
*
* - atomic: If true (default), returns boolean. If false returns array.
* - fieldList: Equivalent to the $fieldList parameter in Model::save()
* - deep: If set to true, all associated data will be validated as well.
*
* @param array $data Record data to validate. This should be a numerically-indexed array
* @param array $options Options to use when validating record data (see above), See also $options of validates().
* @return boolean True on success, or false on failure.
* @return mixed If atomic: True on success, or false on failure.
* Otherwise: array similar to the $data array passed, but values are set to true/false
* depending on whether each record validated successfully.
*/
public function validateMany($data, $options = array()) {
$options = array_merge(array('atomic' => true, 'deep' => false), $options);
$this->validationErrors = $validationErrors = $this->getModel()->validationErrors = $return = array();
foreach ($data as $key => $record) {
if ($options['deep']) {
$validates = $this->validateAssociated($record, $options);
} else {
$validates = $this->getModel()->create($record) && $this->validates($options);
}
if ($validates === false || (is_array($validates) && in_array(false, $validates, true))) {
$validationErrors[$key] = $this->validationErrors;
$validates = false;
} else {
$validates = true;
}
$return[$key] = $validates;
}
$this->validationErrors = $this->getModel()->validationErrors = $validationErrors;
if (!$options['atomic']) {
return $return;
}
if (empty($this->validationErrors)) {
return true;
}
return false;
}
/**
* Returns an array of fields that have failed validation. On the current model.
*
* @param string $options An optional array of custom options to be made available in the beforeValidate callback
* @return array Array of invalid fields
* @see Model::validates()
*/
public function invalidFields($options = array()) {
if (!$this->propagateBeforeValidate($options)) {
return false;
}
$this->data = array();
$this->setOptions($options);
if (!$this->setFields()) {
return $this->getModel()->validationErrors = $this->validationErrors;
}
$this->getData();
$this->getMethods();
$this->setValidationDomain();
foreach ($this->_fields as $field) {
$field->validate();
}
$this->setFields(true);
return $this->getModel()->validationErrors = $this->validationErrors;
}
/**
* Marks a field as invalid, optionally setting the name of validation
* rule (in case of multiple validation for field) that was broken.
*
* @param string $field The name of the field to invalidate
* @param mixed $value Name of validation rule that was not failed, or validation message to
* be returned. If no validation key is provided, defaults to true.
* @return void
*/
public function invalidate($field, $value = true) {
if (!is_array($this->validationErrors)) {
$this->validationErrors = array();
}
$this->validationErrors[$field][] = $this->getModel()->validationErrors[$field][] = $value;
}
/**
* Gets the current data from the model and sets it to $this->data
*
* @param string $field [optional]
* @return array The data
*/
public function getData($field = null, $all = false) {
if (!empty($this->data)) {
if ($field !== null && isset($this->data[$field])) {
return $this->data[$field];
}
return $this->data;
}
$this->data = $this->_model->data;
if (FALSE === $all && isset($this->data[$this->_model->alias])) {
$this->data = $this->data[$this->_model->alias];
} elseif (!is_array($this->data)) {
$this->data = array();
}
if ($field !== null && isset($this->data[$field])) {
return $this->data[$field];
}
return $this->data;
}
/**
* Gets all possible custom methods from the Model, Behaviors and the Validator.
* If $type is null (default) gets all methods. If $type is one of 'model', 'behaviors' or 'validator',
* gets the corresponding methods.
*
* @param string $type [optional] The methods type to get. Defaults to null
* @return array The requested methods
*/
public function getMethods($type = null) {
if (!empty($this->_methods)) {
if ($type !== null && !empty($this->_methods[$type])) {
return $this->_methods[$type];
}
return $this->_methods;
}
$this->_methods['model'] = array_map('strtolower', get_class_methods($this->_model));
$this->_methods['behaviors'] = array_keys($this->_model->Behaviors->methods());
$this->_methods['validator'] = get_class_methods($this);
if ($type !== null && !empty($this->_methods[$type])) {
return $this->_methods[$type];
}
unset($type);
return $this->_methods;
}
/**
* Gets all fields if $name is null (default), or the field for fieldname $name if it's found.
*
* @param string $name [optional] The fieldname to fetch. Defaults to null.
* @return array|ModelField Either the fields array or the ModelField for fieldname $name
*/
public function getFields($name = null) {
if ($name !== null && !empty($this->_fields[$name])) {
return $this->_fields[$name];
}
return $this->_fields;
}
/**
* Sets the ModelField isntances from the Model::$validate property after processing the fieldList and whiteList.
* If Model::$validate is not set or empty, this method returns false. True otherwise.
*
* @param boolean $reset If true will reset the Validator $validate array to the Model's default
* @return boolean True if Model::$validate was processed, false otherwise
*/
public function setFields($reset = false) {
if (!isset($this->_model->validate) || empty($this->_model->validate)) {
$this->_validate = array();
return false;
}
$this->_validate = $this->_model->validate;
if ($reset === true) {
return true;
}
$this->_processWhitelist();
$this->_fields = array();
foreach ($this->_validate as $fieldName => $ruleSet) {
$this->_fields[$fieldName] = new CakeField($this, $fieldName, $ruleSet);
}
unset($fieldName, $ruleSet);
return true;
}
/**
* Sets an options array. If $mergeVars is true, the options will be merged with the existing ones.
* Otherwise they will get replaced. The default is merging the vars.
*
* @param array $options [optional] The options to be set
* @param boolean $mergeVars [optional] If true, the options will be merged, otherwise they get replaced
* @return ModelValidator
*/
public function setOptions($options = array(), $mergeVars = false) {
if ($mergeVars === false) {
$this->options = $options;
} else {
$this->options = array_merge($this->options, $options);
}
return $this;
}
/**
* Sets an option $name with $value. This method is chainable
*
* @param string $name The options name to be set
* @param mixed $value [optional] The value to be set. Defaults to null.
* @return ModelValidator
*/
public function setOption($name, $value = null) {
$this->options[$name] = $value;
return $this;
}
/**
* Gets an options value by $name. If $name is not set or no option has been found, returns null.
*
* @param string $name The options name to look up
* @return mixed Either null or the option value
*/
public function getOptions($name = NULL) {
if (NULL !== $name) {
if (!isset($this->options[$name])) {
return NULL;
}
return $this->options[$name];
}
return $this->options;
}
/**
* Sets the I18n domain for validation messages. This method is chainable.
*
* @param string $validationDomain [optional] The validation domain to be used. If none is given, uses Model::$validationDomain
* @return ModelValidator
*/
public function setValidationDomain($validationDomain = null) {
if ($validationDomain !== null) {
$this->validationDomain = $validationDomain;
} elseif ($this->_model->validationDomain !== null) {
$this->validationDomain = $this->_model->validationDomain;
} else {
$this->validationDomain = ModelValidator::DEFAULT_DOMAIN;
}
return $this;
}
/**
* Gets the parent Model
*
* @return Model
*/
public function getModel() {
return $this->_model;
}
/**
* Processes the Model's whitelist and adjusts the validate array accordingly
*
* @return void
*/
protected function _processWhitelist() {
$whitelist = $this->getModel()->whitelist;
$fieldList = $this->getOptions('fieldList');
if (!empty($fieldList)) {
if (!empty($fieldList[$this->getModel()->alias]) && is_array($fieldList[$this->getModel()->alias])) {
$whitelist = $fieldList[$this->getModel()->alias];
} else {
$whitelist = $fieldList;
}
}
unset($fieldList);
if (!empty($whitelist)) {
$this->validationErrors = array();
$validate = array();
foreach ((array) $whitelist as $f) {
if (!empty($this->_validate[$f])) {
$validate[$f] = $this->_validate[$f];
}
}
$this->_validate = $validate;
}
}
/**
* Runs validation for hasAndBelongsToMany associations that have 'with' keys
* set. And data in the set() data set.
*
* @param array $options Array of options to use on Validation of with models
* @return boolean Failure of validation on with models.
* @see Model::validates()
*/
protected function _validateWithModels($options) {
$valid = true;
$this->getData(null, true);
foreach ($this->getModel()->hasAndBelongsToMany as $assoc => $association) {
if (empty($association['with']) || !isset($this->data[$assoc])) {
continue;
}
list($join) = $this->getModel()->joinModel($this->getModel()->hasAndBelongsToMany[$assoc]['with']);
$data = $this->data[$assoc];
$newData = array();
foreach ((array)$data as $row) {
if (isset($row[$this->getModel()->hasAndBelongsToMany[$assoc]['associationForeignKey']])) {
$newData[] = $row;
} elseif (isset($row[$join]) && isset($row[$join][$this->getModel()->hasAndBelongsToMany[$assoc]['associationForeignKey']])) {
$newData[] = $row[$join];
}
}
if (empty($newData)) {
continue;
}
foreach ($newData as $data) {
$data[$this->getModel()->hasAndBelongsToMany[$assoc]['foreignKey']] = $this->getModel()->id;
$this->getModel()->{$join}->create($data);
$valid = ($valid && $this->getModel()->{$join}->getValidator()->validates($options));
}
}
return $valid;
}
/**
* Propagates the beforeValidate event
*
* @param array $options
* @return boolean
*/
public function propagateBeforeValidate($options = array()) {
$event = new CakeEvent('Model.beforeValidate', $this->getModel(), array($options));
list($event->break, $event->breakOn) = array(true, false);
$this->getModel()->getEventManager()->dispatch($event);
if ($event->isStopped()) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,188 @@
<?php
/**
* ModelValidator.
*
* Provides the Model validation logic.
*
* PHP versions 5
*
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @package Cake.Model
* @since CakePHP(tm) v 3.0.0
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
App::uses('ModelValidator', 'Model');
App::uses('CakeRule', 'Model/Validator');
/**
* ModelField object.
*
* @package Cake.Model
* @link http://book.cakephp.org/2.0/en/data-validation.html
*/
class CakeField {
/**
* Holds the parent Validator instance
*
* @var ModelValidator
*/
protected $_validator = null;
/**
* Holds the ValidationRule objects
*
* @var array
*/
protected $_rules = array();
/**
* If the validation is stopped
*
* @var boolean
*/
public $isStopped = false;
/**
* Holds the fieldname
*
* @var string
*/
public $field = null;
/**
* Holds the original ruleSet
*
* @var array
*/
public $ruleSet = array();
/**
* Constructor
*
* @param ModelValidator $validator The parent ModelValidator
* @param string $fieldName The fieldname
* @param
*/
public function __construct(ModelValidator $validator, $fieldName, $ruleSet) {
$this->_validator = $validator;
$this->data = &$this->getValidator()->data;
$this->field = $fieldName;
if (!is_array($ruleSet) || (is_array($ruleSet) && isset($ruleSet['rule']))) {
$ruleSet = array($ruleSet);
}
foreach ($ruleSet as $index => $validateProp) {
$this->_rules[$index] = new CakeRule($this, $validateProp, $index);
}
$this->ruleSet = $ruleSet;
unset($ruleSet, $validateProp);
}
/**
* Validates a ModelField
*
* @return mixed
*/
public function validate() {
foreach ($this->getRules() as $rule) {
if ($rule->skip()) {
continue;
}
$rule->isRequired();
if (!$rule->checkRequired() && array_key_exists($this->field, $this->data)) {
if ($rule->checkEmpty()) {
break;
}
$rule->dispatchValidation();
}
if ($rule->checkRequired() || !$rule->isValid()) {
$this->getValidator()->invalidate($this->field, $rule->getMessage());
if ($rule->isLast()) {
return false;
}
}
}
return true;
}
/**
* Gets a rule for a certain index
*
* @param mixed index
* @return ValidationRule
*/
public function getRule($index) {
if (!empty($this->_rules[$index])) {
return $this->_rules[$index];
}
}
/**
* Gets all rules for this ModelField
*
* @return array
*/
public function getRules() {
return $this->_rules;
}
/**
* Sets a ValidationRule $rule for key $key
*
* @param mixed $key The key under which the rule should be set
* @param ValidationRule $rule The ValidationRule to be set
* @return ModelField
*/
public function setRule($key, CakeRule $rule) {
$this->_rules[$key] = $rule;
return $this;
}
/**
* Sets the rules for a given field
*
* @param array $rules The rules to be set
* @param bolean $mergeVars [optional] If true, merges vars instead of replace. Defaults to true.
* @return ModelField
*/
public function setRules($rules = array(), $mergeVars = true) {
if ($mergeVars === false) {
$this->_rules = $rules;
} else {
$this->_rules = array_merge($this->_rules, $rules);
}
return $this;
}
/**
* Gets the validator this field is atached to
*
* @return ModelValidator The parent ModelValidator instance
*/
public function getValidator() {
return $this->_validator;
}
/**
* Magic isset
*
* @return true if the field exists in data, false otherwise
*/
public function __isset($fieldName) {
return array_key_exists($fieldName, $this->getValidator()->getData());
}
}

View file

@ -0,0 +1,404 @@
<?php
/**
* CakeRule.
*
* Provides the Model validation logic.
*
* PHP versions 5
*
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @package Cake.Model
* @since CakePHP(tm) v 3.0.0
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
App::uses('ModelValidator', 'Model');
App::uses('CakeField', 'Model/Validator');
App::uses('Validation', 'Utility');
/**
* ValidationRule object.
*
* @package Cake.Model
* @link http://book.cakephp.org/2.0/en/data-validation.html
*/
class CakeRule {
/**
* Holds a reference to the parent field
*
* @var ModelField
*/
protected $_field = null;
/**
* Has the required check failed?
*
* @var boolean
*/
protected $_requiredFail = null;
/**
* The 'valid' value
*
* @var mixed
*/
protected $_valid = true;
/**
* Holds the index under which the Vaildator was attached
*
* @var mixed
*/
protected $_index = null;
/**
* Create or Update transaction?
*
* @var boolean
*/
protected $_modelExists = null;
/**
* The parsed rule
*
* @var mixed
*/
protected $_rule = null;
/**
* The parsed rule parameters
*
* @var array
*/
protected $_ruleParams = array();
/**
* The errorMessage
*
* @var string
*/
protected $_errorMessage = null;
/**
* Holds passed in options
*
* @var array
*/
protected $_passedOptions = array();
/**
* Flag indicating wether the allowEmpty check has failed
*
* @var boolean
*/
protected $_emptyFail = null;
/**
* The 'rule' key
*
* @var mixed
*/
public $rule = 'blank';
/**
* The 'required' key
*
* @var mixed
*/
public $required = null;
/**
* The 'allowEmpty' key
*
* @var boolean
*/
public $allowEmpty = false;
/**
* The 'on' key
*
* @var string
*/
public $on = null;
/**
* The 'last' key
*
* @var boolean
*/
public $last = true;
/**
* The 'message' key
*
* @var string
*/
public $message = null;
/**
* Constructor
*
* @param ModelField $field
* @param array $validator [optional] The validator properties
* @param mixed $index [optional]
*/
public function __construct(CakeField $field, $validator = array(), $index = null) {
$this->_field = $field;
$this->_index = $index;
unset($field, $index);
$this->data = &$this->getField()
->data;
$this->_modelExists = $this->getField()
->getValidator()
->getModel()
->exists();
$this->_addValidatorProps($validator);
unset($validator);
}
/**
* Checks if the rule is valid
*
* @return boolean
*/
public function isValid() {
if (!$this->_valid || (is_string($this->_valid) && strlen($this->_valid) > 0)) {
return false;
}
return true;
}
/**
* Checks if the field is required by the 'required' value
*
* @return boolean
*/
public function isRequired() {
if ($this->required === true || $this->required === false) {
return $this->required;
}
if (in_array($this->required, array('create', 'update'), true)) {
if ($this->required === 'create' && !$this->_modelExists || $this->required === 'update' && $this->_modelExists) {
$this->required = true;
}
}
return $this->required;
}
/**
* Checks if the field failed the required validation
*
* @return boolean
*/
public function checkRequired() {
if ($this->_requiredFail !== null) {
return $this->_requiredFail;
}
$this->_requiredFail = (
(!isset($this->data[$this->getField()->field]) && $this->required === true) ||
(
isset($this->data[$this->getField()->field]) && (empty($this->data[$this->getField()->field]) &&
!is_numeric($this->data[$this->getField()->field])) && $this->allowEmpty === false
)
);
return $this->_requiredFail;
}
/**
* Checks if the allowEmpty key applies
*
* @return boolean
*/
public function checkEmpty() {
if ($this->_emptyFail !== null) {
return $this->_emptyFail;
}
$this->_emptyFail = false;
if (empty($this->data[$this->getField()->field]) && $this->data[$this->getField()->field] != '0' && $this->allowEmpty === true) {
$this->_emptyFail = true;
}
return $this->_emptyFail;
}
/**
* Checks if the Validation rule can be skipped
*
* @return boolean True if the ValidaitonRule can be skipped
*/
public function skip() {
if (!empty($this->on)) {
if ($this->on == 'create' && $this->_modelExists || $this->on == 'update' && !$this->_modelExists) {
return true;
}
}
return false;
}
/**
* Checks if the 'last' key is true
*
* @return boolean
*/
public function isLast() {
return (bool) $this->last;
}
/**
* Gets the validation error message
*
* @return string
*/
public function getMessage() {
return $this->_processValidationResponse();
}
/**
* Gets the parent field
*
* @return ModelField
*/
public function getField() {
return $this->_field;
}
/**
* Gets an array with the rule properties
*
* @return array
*/
public function getPropertiesArray() {
return array(
'rule' => $this->rule,
'required' => $this->required,
'allowEmpty' => $this->allowEmpty,
'on' => $this->on,
'last' => $this->last,
'message' => $this->message
);
}
/**
* Dispatches the validation rule to the given validator method
*
* @return boolean True if the rule could be dispatched, false otherwise
*/
public function dispatchValidation() {
$this->_parseRule();
$validator = $this->getPropertiesArray();
$methods = $this->getField()->getValidator()->getMethods();
$Model = $this->getField()->getValidator()->getModel();
if (in_array(strtolower($this->_rule), $methods['model'])) {
$this->_ruleParams[] = array_merge($validator, $this->_passedOptions);
$this->_ruleParams[0] = array($this->getField()->field => $this->_ruleParams[0]);
$this->_valid = $Model->dispatchMethod($this->_rule, $this->_ruleParams);
} elseif (in_array($this->_rule, $methods['behaviors']) || in_array(strtolower($this->_rule), $methods['behaviors'])) {
$this->_ruleParams[] = array_merge($validator, $this->_passedOptions);
$this->_ruleParams[0] = array($this->getField()->field => $this->_ruleParams[0]);
$this->_valid = $Model->Behaviors->dispatchMethod($Model, $this->_rule, $this->_ruleParams);
} elseif (method_exists('Validation', $this->_rule)) {
$this->_valid = call_user_func_array(array('Validation', $this->_rule), $this->_ruleParams);
} elseif (!is_array($validator['rule'])) {
$this->_valid = preg_match($this->_rule, $this->data[$this->getField()->field]);
} elseif (Configure::read('debug') > 0) {
trigger_error(__d('cake_dev', 'Could not find validation handler %s for %s', $this->_rule, $this->_field->field), E_USER_WARNING);
return false;
}
unset($validator, $methods, $Model);
return true;
}
/**
* Fetches the correct error message for a failed validation
*
* @return string
*/
protected function _processValidationResponse() {
$validationDomain = $this->_field->getValidator()->validationDomain;
if (is_string($this->_valid)) {
$this->_errorMessage = $this->_valid;
} elseif ($this->message !== null) {
$args = null;
if (is_array($this->message)) {
$this->_errorMessage = $this->message[0];
$args = array_slice($this->message, 1);
} else {
$this->_errorMessage = $this->message;
}
if (is_array($this->rule) && $args === null) {
$args = array_slice($this->getField()->ruleSet[$this->_index]['rule'], 1);
}
$this->_errorMessage = __d($validationDomain, $this->_errorMessage, $args);
} elseif (is_string($this->_index)) {
if (is_array($this->rule)) {
$args = array_slice($this->getField()->ruleSet[$this->_index]['rule'], 1);
$this->_errorMessage = __d($validationDomain, $this->_index, $args);
} else {
$this->_errorMessage = __d($validationDomain, $this->_index);
}
} elseif (!$this->checkRequired() && is_numeric($this->_index) && count($this->getField()->ruleSet) > 1) {
$this->_errorMessage = $this->_index + 1;
} else {
$this->_errorMessage = __d('cake_dev', 'This field cannot be left blank');
}
unset($validationDomain);
return $this->_errorMessage;
}
/**
* Sets the rule properties from the rule entry in validate
*
* @param array $validator [optional]
* @return void
*/
protected function _addValidatorProps($validator = array()) {
if (!is_array($validator)) {
$validator = array('rule' => $validator);
}
foreach ($validator as $key => $value) {
if (isset($value) || !empty($value)) {
if (in_array($key, array('rule', 'required', 'allowEmpty', 'on', 'message', 'last'))) {
$this->$key = $validator[$key];
} else {
$this->_passedOptions[$key] = $value;
}
}
}
unset($validator);
}
/**
* Parses the rule and sets the rule and ruleParams
*
* @return void
*/
protected function _parseRule() {
if (is_array($this->rule)) {
$this->_rule = $this->rule[0];
unset($this->rule[0]);
$this->_ruleParams = array_merge(array($this->data[$this->getField()->field]), array_values($this->rule));
} else {
$this->_rule = $this->rule;
$this->_ruleParams = array($this->data[$this->getField()->field]);
}
}
}

View file

@ -387,7 +387,7 @@ class AnotherTestController extends ControllerTestAppController {
/**
* merge parent
*
*
* @var string
*/
protected $_mergeParent = 'ControllerTestAppController';

View file

@ -48,7 +48,8 @@ class ModelValidationTest extends BaseModelTest {
'on' => null,
'last' => true,
'allowEmpty' => false,
'required' => true
'required' => true,
'message' => null
),
'or' => true,
'ignoreOnSame' => 'id'
@ -84,7 +85,8 @@ class ModelValidationTest extends BaseModelTest {
'on' => null,
'last' => true,
'allowEmpty' => false,
'required' => true
'required' => true,
'message' => null
),
'six' => 6
);
@ -110,7 +112,8 @@ class ModelValidationTest extends BaseModelTest {
'on' => null,
'last' => true,
'allowEmpty' => false,
'required' => true
'required' => true,
'message' => null
)
);
$this->assertEquals($expected, $TestModel->validatorParams);
@ -346,6 +349,7 @@ class ModelValidationTest extends BaseModelTest {
$result = $TestModel->create($data);
$this->assertEquals($data, $result);
$result = $TestModel->validates();
$this->assertTrue($result);
$data = array('TestValidate' => array(

View file

@ -107,7 +107,7 @@ class ModelWriteTest extends BaseModelTest {
$testResult = $Article->find('first', array('conditions' => array('Article.title' => 'Test Title')));
$this->assertEquals($testResult['Article']['title'], $data['Article']['title']);
$this->assertEquals($data['Article']['title'], $testResult['Article']['title']);
$this->assertEquals('2008-01-01 00:00:00', $testResult['Article']['created']);
}
@ -151,8 +151,8 @@ class ModelWriteTest extends BaseModelTest {
$TestModel->save(array('title' => 'Test record'));
$result = $TestModel->findByTitle('Test record');
$this->assertEquals(
array_keys($result['Uuid']),
array('id', 'title', 'count', 'created', 'updated')
array('id', 'title', 'count', 'created', 'updated'),
array_keys($result['Uuid'])
);
$this->assertEquals(36, strlen($result['Uuid']['id']));
}
@ -173,8 +173,8 @@ class ModelWriteTest extends BaseModelTest {
$TestModel->save(array('title' => 'Test record', 'id' => null));
$result = $TestModel->findByTitle('Test record');
$this->assertEquals(
array_keys($result['Uuid']),
array('id', 'title', 'count', 'created', 'updated')
array('id', 'title', 'count', 'created', 'updated'),
array_keys($result['Uuid'])
);
$this->assertEquals(36, strlen($result['Uuid']['id']));
}
@ -2345,7 +2345,7 @@ class ModelWriteTest extends BaseModelTest {
'User' => array(
'user' => 'updated user'
)));
$this->assertEquals($TestModel->id, $id);
$this->assertEquals($id, $TestModel->id);
$result = $TestModel->findById($id);
$this->assertEquals('updated user', $result['User']['user']);
@ -2940,7 +2940,7 @@ class ModelWriteTest extends BaseModelTest {
$model->Attachment->validate = array('attachment' => 'notEmpty');
$model->Attachment->bindModel(array('belongsTo' => array('Comment')));
$this->assertEquals($model->saveAll(
$result = $model->saveAll(
array(
'Comment' => array(
'comment' => '',
@ -2950,7 +2950,8 @@ class ModelWriteTest extends BaseModelTest {
'Attachment' => array('attachment' => '')
),
array('validate' => 'first')
), false);
);
$this->assertEquals(false, $result);
$expected = array(
'Comment' => array('comment' => array('This field cannot be left blank')),
'Attachment' => array('attachment' => array('This field cannot be left blank'))
@ -4329,7 +4330,7 @@ class ModelWriteTest extends BaseModelTest {
$this->assertTrue(Set::matches('/Post[2][title=Just update the title]', $result));
}
$this->assertEquals($TestModel->validationErrors, $errors);
$this->assertEquals($errors, $TestModel->validationErrors);
$TestModel->validate = array('title' => 'notEmpty', 'author_id' => 'numeric');
$data = array(
@ -4400,7 +4401,7 @@ class ModelWriteTest extends BaseModelTest {
$result[3]['Post']['updated'], $result[3]['Post']['created']
);
$this->assertEquals($expected, $result);
$this->assertEquals($TestModel->validationErrors, $errors);
$this->assertEquals($errors, $TestModel->validationErrors);
$data = array(
array(
@ -4422,7 +4423,7 @@ class ModelWriteTest extends BaseModelTest {
$result[3]['Post']['updated'], $result[3]['Post']['created']
);
$this->assertEquals($expected, $result);
$this->assertEquals($TestModel->validationErrors, $errors);
$this->assertEquals($errors, $TestModel->validationErrors);
}
/**
@ -4550,8 +4551,8 @@ class ModelWriteTest extends BaseModelTest {
$result = $model->find('all');
$this->assertEquals(
$result[0]['Article']['title'],
'Post with Author saveAlled from comment'
'Post with Author saveAlled from comment',
$result[0]['Article']['title']
);
$this->assertEquals('Only new comment', $result[0]['Comment'][0]['comment']);
}
@ -5037,7 +5038,7 @@ class ModelWriteTest extends BaseModelTest {
$model->Attachment->validate = array('attachment' => 'notEmpty');
$model->Attachment->bindModel(array('belongsTo' => array('Comment')));
$this->assertEquals($model->saveAssociated(
$result = $model->saveAssociated(
array(
'Comment' => array(
'comment' => '',
@ -5046,7 +5047,8 @@ class ModelWriteTest extends BaseModelTest {
),
'Attachment' => array('attachment' => '')
)
), false);
);
$this->assertFalse($result);
$expected = array(
'Comment' => array('comment' => array('This field cannot be left blank')),
'Attachment' => array('attachment' => array('This field cannot be left blank'))
@ -5688,7 +5690,7 @@ class ModelWriteTest extends BaseModelTest {
$this->assertTrue(Set::matches('/Post[2][title=Just update the title]', $result));
}
$this->assertEquals($TestModel->validationErrors, $errors);
$this->assertEquals($errors, $TestModel->validationErrors);
$TestModel->validate = array('title' => 'notEmpty', 'author_id' => 'numeric');
$data = array(
@ -5746,7 +5748,7 @@ class ModelWriteTest extends BaseModelTest {
'published' => 'N',
)));
$this->assertEquals($expected, $result);
$this->assertEquals($TestModel->validationErrors, $errors);
$this->assertEquals($errors, $TestModel->validationErrors);
$data = array(
array(
@ -5768,7 +5770,7 @@ class ModelWriteTest extends BaseModelTest {
'order' => 'Post.id ASC'
));
$this->assertEquals($expected, $result);
$this->assertEquals($TestModel->validationErrors, $errors);
$this->assertEquals($errors, $TestModel->validationErrors);
}
/**
@ -5876,8 +5878,8 @@ class ModelWriteTest extends BaseModelTest {
$result = $model->find('all');
$this->assertEquals(
$result[0]['Article']['title'],
'Post with Author saveAlled from comment'
'Post with Author saveAlled from comment',
$result[0]['Article']['title']
);
$this->assertEquals('Only new comment', $result[0]['Comment'][0]['comment']);
}