diff --git a/Cake/ORM/Table.php b/Cake/ORM/Table.php index 531ad098278..537abed27c0 100644 --- a/Cake/ORM/Table.php +++ b/Cake/ORM/Table.php @@ -134,6 +134,13 @@ class Table { */ protected $_entityClass; +/** + * A list of validation objects indexed by name + * + * @var array + */ + protected $_validators = []; + /** * Initializes a new instance * @@ -734,6 +741,70 @@ public function updateAll($fields, $conditions) { return $success; } +/** + * Returns the validation rules tagged with $name. It is possible to have + * multiple different named validation sets, this is useful when you need + * to use varying rules when saving from different routines in your system. + * + * There are two different ways of creating and naming validation sets: by + * creating a new method inside your own Table subclass, or by building + * the validator object yourself and storing it using this method. + * + * For example, if you wish to create a validation set called 'forSubscription', + * you will need to create a method in your Table subclass as follows: + * + * {{{ + * public function validationForSubscription($validator) { + * return $validator + * ->add('email', 'required', ['rule' => 'email', 'required' => true]) + * ->add('password', 'valid', ['rule' => 'notEmpty', 'required' => true]); + * } + * }}} + * + * Otherwise, you can build the object by yourself and store it in the Table object: + * + * {{{ + * $validator = new \Cake\ORM\Validator($table); + * $validator + * ->add('email', 'required', ['rule' => 'email', 'required' => true]) + * ->add('password', 'valid', ['rule' => 'notEmpty', 'required' => true]); + * $table->validator('forSubscription', $validator); + * }}} + * + * You can implement the method in `validationDefault` in your Table subclass + * should you wish to have a validation set that applies in cases where no other + * set is specified. + * + * @param string $name the name of the validation set to return + * @param \Cake\ORM\Validator $validator + * @return \Cake\ORM\Validator + */ + public function validator($name = 'default', $instance = null) { + if ($instance === null && isset($this->_validators[$name])) { + return $this->_validators[$name]; + } + + if ($instance !== null) { + return $this->_validators[$name] = $instance; + } + + $validator = new Validator; + $validator = $this->{'validation' . $name}($validator); + return $this->_validators[$name] = $validator; + } + +/** + * Returns the default validator object. Subclasses can override this function + * to add a default validation set to the validator object. + * + * @param \Cake\ORM\Validator $validator The validator that can be modified to + * add some rules to it. + * @return \Cake\ORM\Validator + */ + public function validationDefault(Validator $validator) { + return $validator; + } + /** * Delete all matching rows. * diff --git a/Cake/ORM/Validator.php b/Cake/ORM/Validator.php index 6bb95ddff91..ee36ddd57f7 100644 --- a/Cake/ORM/Validator.php +++ b/Cake/ORM/Validator.php @@ -1,8 +1,6 @@ _model = $Model; + public function __construct(Table $table) { + $this->_model = $model; } /** - * Returns true if all fields pass validation. Will validate hasAndBelongsToMany associations - * that use the 'with' key as well. Since `Model::_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. + * Returns true if all fields pass validation. * - * @param array $options An optional array of custom options to be made available in the beforeValidate callback + * @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()) { @@ -106,129 +100,6 @@ public function validates($options = array()) { 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. - * - * Warning: This method could potentially change the passed argument `$data`, - * If you do not want this to happen, make a copy of `$data` before passing it - * to this method - * - * @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()) { - $model = $this->getModel(); - $options = array_merge(array('atomic' => true, 'deep' => false), $options); - $model->validationErrors = $validationErrors = $return = array(); - $model->create(null); - $return[$model->alias] = true; - if (!($model->set($data) && $model->validates($options))) { - $validationErrors[$model->alias] = $model->validationErrors; - $return[$model->alias] = false; - } - $data = $model->data; - if (!empty($options['deep']) && isset($data[$model->alias])) { - $recordData = $data[$model->alias]; - unset($data[$model->alias]); - $data += $recordData; - } - - $associations = $model->getAssociated(); - foreach ($data as $association => &$values) { - $validates = true; - if (isset($associations[$association])) { - if (in_array($associations[$association], array('belongsTo', 'hasOne'))) { - if ($options['deep']) { - $validates = $model->{$association}->validateAssociated($values, $options); - } else { - $model->{$association}->create(null); - $validates = $model->{$association}->set($values) && $model->{$association}->validates($options); - $data[$association] = $model->{$association}->data[$model->{$association}->alias]; - } - if (is_array($validates)) { - $validates = !in_array(false, Hash::flatten($validates), true); - } - $return[$association] = $validates; - } elseif ($associations[$association] === 'hasMany') { - $validates = $model->{$association}->validateMany($values, $options); - $return[$association] = $validates; - } - if (!$validates || (is_array($validates) && in_array(false, $validates, true))) { - $validationErrors[$association] = $model->{$association}->validationErrors; - } - } - } - - $model->validationErrors = $validationErrors; - if (isset($validationErrors[$model->alias])) { - $model->validationErrors = $validationErrors[$model->alias]; - unset($validationErrors[$model->alias]); - $model->validationErrors = array_merge($model->validationErrors, $validationErrors); - } - if (!$options['atomic']) { - return $return; - } - if ($return[$model->alias] === false || !empty($model->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. - * - * Warning: This method could potentially change the passed argument `$data`, - * If you do not want this to happen, make a copy of `$data` before passing it - * to this method - * - * @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 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()) { - $model = $this->getModel(); - $options = array_merge(array('atomic' => true, 'deep' => false), $options); - $model->validationErrors = $validationErrors = $return = array(); - foreach ($data as $key => &$record) { - if ($options['deep']) { - $validates = $model->validateAssociated($record, $options); - } else { - $model->create(null); - $validates = $model->set($record) && $model->validates($options); - $data[$key] = $model->data; - } - if ($validates === false || (is_array($validates) && in_array(false, Hash::flatten($validates), true))) { - $validationErrors[$key] = $model->validationErrors; - $validates = false; - } else { - $validates = true; - } - $return[$key] = $validates; - } - $model->validationErrors = $validationErrors; - if (!$options['atomic']) { - return $return; - } - return empty($model->validationErrors); - } - /** * Returns an array of fields that have failed validation. On the current model. This method will * actually run validation rules over data, not just return the messages. @@ -242,11 +113,6 @@ public function errors($options = array()) { return false; } $model = $this->getModel(); - - if (!$this->_parseRules()) { - return $model->validationErrors; - } - $fieldList = $model->whitelist; if (empty($fieldList) && !empty($options['fieldList'])) { if (!empty($options['fieldList'][$model->alias]) && is_array($options['fieldList'][$model->alias])) { @@ -321,7 +187,6 @@ public function getMethods() { * @return Cake\Model\Validator\ValidationSet|array */ public function getField($name = null) { - $this->_parseRules(); if ($name !== null) { if (!empty($this->_fields[$name])) { return $this->_fields[$name]; @@ -331,33 +196,6 @@ public function getField($name = null) { return $this->_fields; } -/** - * Sets the Cake ValidationSet objects from the `Model::$validate` property - * If `Model::$validate` is not set or empty, this method returns false. True otherwise. - * - * @return boolean true if `Model::$validate` was processed, false otherwise - */ - protected function _parseRules() { - if ($this->_validate === $this->_model->validate) { - return true; - } - - if (empty($this->_model->validate)) { - $this->_validate = array(); - $this->_fields = array(); - return false; - } - - $this->_validate = $this->_model->validate; - $this->_fields = array(); - $methods = $this->getMethods(); - foreach ($this->_validate as $fieldName => $ruleSet) { - $this->_fields[$fieldName] = new ValidationSet($fieldName, $ruleSet); - $this->_fields[$fieldName]->setMethods($methods); - } - return true; - } - /** * Sets the I18n domain for validation messages. This method is chainable. * @@ -403,42 +241,6 @@ protected function _validationList($fieldList = array()) { return $validateList; } -/** - * Runs validation for hasAndBelongsToMany associations that have 'with' keys - * set and data in the 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; - $model = $this->getModel(); - - foreach ($model->hasAndBelongsToMany as $assoc => $association) { - if (empty($association['with']) || !isset($model->data[$assoc])) { - continue; - } - list($join) = $model->joinModel($model->hasAndBelongsToMany[$assoc]['with']); - $data = $model->data[$assoc]; - - $newData = array(); - foreach ((array)$data as $row) { - if (isset($row[$model->hasAndBelongsToMany[$assoc]['associationForeignKey']])) { - $newData[] = $row; - } elseif (isset($row[$join]) && isset($row[$join][$model->hasAndBelongsToMany[$assoc]['associationForeignKey']])) { - $newData[] = $row[$join]; - } - } - foreach ($newData as $data) { - $data[$model->hasAndBelongsToMany[$assoc]['foreignKey']] = $model->id; - $model->{$join}->create($data); - $valid = ($valid && $model->{$join}->validator()->validates($options)); - } - } - return $valid; - } - /** * Propagates beforeValidate event * @@ -463,7 +265,6 @@ protected function _triggerBeforeValidate($options = array()) { * @return boolean */ public function offsetExists($field) { - $this->_parseRules(); return isset($this->_fields[$field]); } @@ -474,7 +275,6 @@ public function offsetExists($field) { * @return Cake\Model\Validator\ValidationSet */ public function offsetGet($field) { - $this->_parseRules(); return $this->_fields[$field]; } @@ -486,7 +286,6 @@ public function offsetGet($field) { * @return void */ public function offsetSet($field, $rules) { - $this->_parseRules(); if (!$rules instanceof ValidationSet) { $rules = new ValidationSet($field, $rules); $methods = $this->getMethods(); @@ -502,7 +301,6 @@ public function offsetSet($field, $rules) { * @return void */ public function offsetUnset($field) { - $this->_parseRules(); unset($this->_fields[$field]); } @@ -512,7 +310,6 @@ public function offsetUnset($field) { * @return ArrayIterator */ public function getIterator() { - $this->_parseRules(); return new \ArrayIterator($this->_fields); } @@ -522,7 +319,6 @@ public function getIterator() { * @return integer */ public function count() { - $this->_parseRules(); return count($this->_fields); } @@ -550,7 +346,6 @@ public function count() { * @return ModelValidator this instance */ public function add($field, $name, $rule = null) { - $this->_parseRules(); if ($name instanceof ValidationSet) { $this->_fields[$field] = $name; return $this; @@ -589,7 +384,6 @@ public function add($field, $name, $rule = null) { * @return ModelValidator this instance */ public function remove($field, $rule = null) { - $this->_parseRules(); if ($rule === null) { unset($this->_fields[$field]); } else {