diff --git a/framework/base/Model.php b/framework/base/Model.php index b2da4e2..a7c3433 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -10,6 +10,8 @@ namespace yii\base; use yii\util\StringHelper; +use yii\validators\Validator; +use yii\validators\RequiredValidator; /** * Model is the base class for data models. @@ -35,7 +37,6 @@ use yii\util\StringHelper; * @property array $errors Errors for all attributes or the specified attribute. Empty array is returned if no error. * @property array $attributes Attribute values (name=>value). * @property string $scenario The scenario that this model is in. - * @property array $safeAttributeNames Safe attribute names in the current [[scenario]]. * * @event ModelEvent beforeValidate an event raised at the beginning of [[validate()]]. You may set * [[ModelEvent::isValid]] to be false to stop the validation. @@ -48,43 +49,8 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess { private static $_attributes = array(); // class name => array of attribute names private $_errors; // attribute name => array of errors - private $_validators; // validators - private $_scenario; // scenario - - /** - * Constructor. - * @param string|null $scenario name of the [[scenario]] that this model is used in. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($scenario = null, $config = array()) - { - $this->_scenario = $scenario; - parent::__construct($config); - } - - /** - * Returns the list of attribute names. - * By default, this method returns all public non-static properties of the class. - * You may override this method to change the default behavior. - * @return array list of attribute names. - */ - public function attributeNames() - { - $className = get_class($this); - if (isset(self::$_attributes[$className])) { - return self::$_attributes[$className]; - } - - $class = new \ReflectionClass($this); - $names = array(); - foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { - $name = $property->getName(); - if (!$property->isStatic()) { - $names[] = $name; - } - } - return self::$_attributes[$className] = $names; - } + private $_validators; // Vector of validators + private $_scenario = 'default'; /** * Returns the validation rules for attributes. @@ -107,7 +73,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * * - attribute list: required, specifies the attributes (separated by commas) to be validated; * - validator type: required, specifies the validator to be used. It can be the name of a model - * class method, the name of a built-in validator, or a validator class (or its path alias). + * class method, the name of a built-in validator, or a validator class name (or its path alias). * - on: optional, specifies the [[scenario|scenarios]] (separated by commas) when the validation * rule can be applied. If this option is not set, the rule will apply to all scenarios. * - additional name-value pairs can be specified to initialize the corresponding validator properties. @@ -145,6 +111,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * merge the parent rules with child rules using functions such as `array_merge()`. * * @return array validation rules + * @see scenarios */ public function rules() { @@ -152,6 +119,56 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** + * Returns a list of scenarios and the corresponding relevant attributes. + * The returned array should be in the following format: + * + * ~~~ + * array( + * 'scenario1' => array('attribute11', 'attribute12', ...), + * 'scenario2' => array('attribute21', 'attribute22', ...), + * ... + * ) + * ~~~ + * + * Attributes relevant to the current scenario are considered safe and can be + * massively assigned. When [[validate()]] is invoked, these attributes will + * be validated using the rules declared in [[rules()]]. + * + * If an attribute should NOT be massively assigned (thus considered unsafe), + * please prefix the attribute with an exclamation character (e.g. '!attribute'). + * + * @return array a list of scenarios and the corresponding relevant attributes. + */ + public function scenarios() + { + return array(); + } + + /** + * Returns the list of attribute names. + * By default, this method returns all public non-static properties of the class. + * You may override this method to change the default behavior. + * @return array list of attribute names. + */ + public function attributes() + { + $className = get_class($this); + if (isset(self::$_attributes[$className])) { + return self::$_attributes[$className]; + } + + $class = new \ReflectionClass($this); + $names = array(); + foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { + $name = $property->getName(); + if (!$property->isStatic()) { + $names[] = $name; + } + } + return self::$_attributes[$className] = $names; + } + + /** * Returns the attribute labels. * * Attribute labels are mainly used for display purpose. For example, given an attribute @@ -175,30 +192,33 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Performs the data validation. * - * This method executes the validation rules as declared in [[rules()]]. - * Only the rules applicable to the current [[scenario]] will be executed. - * A rule is considered applicable to a scenario if its `on` option is not set - * or contains the scenario. + * This method executes the validation rules applicable to the current [[scenario]]. + * The following criteria are used to determine whether a rule is currently applicable: + * + * - the rule must be associated with the attributes relevant to the current scenario; + * - the rules must be effective for the current scenario. * * This method will call [[beforeValidate()]] and [[afterValidate()]] before and - * after actual validation, respectively. If [[beforeValidate()]] returns false, - * the validation and [[afterValidate()]] will be cancelled. + * after the actual validation, respectively. If [[beforeValidate()]] returns false, + * the validation will be cancelled and [[afterValidate()]] will not be called. * - * Errors found during the validation can be retrieved via [[getErrors()]]. + * Errors found during the validation can be retrieved via [[getErrors()]] + * and [[getError()]]. * * @param array $attributes list of attributes that should be validated. * If this parameter is empty, it means any attribute listed in the applicable * validation rules should be validated. * @param boolean $clearErrors whether to call [[clearErrors()]] before performing validation * @return boolean whether the validation is successful without any error. - * @see beforeValidate() - * @see afterValidate() */ public function validate($attributes = null, $clearErrors = true) { if ($clearErrors) { $this->clearErrors(); } + if ($attributes === null) { + $attributes = $this->activeAttributes(); + } if ($this->beforeValidate()) { foreach ($this->getActiveValidators() as $validator) { $validator->validate($this, $attributes); @@ -214,7 +234,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * The default implementation raises a `beforeValidate` event. * You may override this method to do preliminary checks before validation. * Make sure the parent implementation is invoked so that the event can be raised. - * @return boolean whether validation should be executed. Defaults to true. + * @return boolean whether the validation should be executed. Defaults to true. * If false is returned, the validation will stop and the model is considered invalid. */ public function beforeValidate() @@ -269,8 +289,9 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess { $validators = array(); $scenario = $this->getScenario(); + /** @var $validator Validator */ foreach ($this->getValidators() as $validator) { - if ($validator->applyTo($scenario, $attribute)) { + if ($validator->isActive($scenario, $attribute)) { $validators[] = $validator; } } @@ -287,8 +308,10 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess { $validators = new Vector; foreach ($this->rules() as $rule) { - if (isset($rule[0], $rule[1])) { // attributes, validator type - $validator = \yii\validators\Validator::createValidator($rule[1], $this, $rule[0], array_slice($rule, 2)); + if ($rule instanceof Validator) { + $validators->add($rule); + } elseif (isset($rule[0], $rule[1])) { // attributes, validator type + $validator = Validator::createValidator($rule[1], $this, $rule[0], array_slice($rule, 2)); $validators->add($validator); } else { throw new BadConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.'); @@ -308,7 +331,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess public function isAttributeRequired($attribute) { foreach ($this->getActiveValidators($attribute) as $validator) { - if ($validator instanceof \yii\validators\RequiredValidator) { + if ($validator instanceof RequiredValidator) { return true; } } @@ -322,13 +345,8 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess */ public function isAttributeSafe($attribute) { - $validators = $this->getActiveValidators($attribute); - foreach ($validators as $validator) { - if (!$validator->safe) { - return false; - } - } - return $validators !== array(); + $scenarios = $this->scenarios(); + return in_array($attribute, $scenarios[$this->getScenario()], true); } /** @@ -346,7 +364,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Returns a value indicating whether there is any validation error. - * @param string $attribute attribute name. Use null to check all attributes. + * @param string|null $attribute attribute name. Use null to check all attributes. * @return boolean whether there is any error. */ public function hasErrors($attribute = null) @@ -452,7 +470,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Returns attribute values. * @param array $names list of attributes whose value needs to be returned. - * Defaults to null, meaning all attributes listed in [[attributeNames()]] will be returned. + * Defaults to null, meaning all attributes listed in [[attributes()]] will be returned. * If it is an array, only the attributes in the array will be returned. * @return array attribute values (name=>value). */ @@ -461,13 +479,13 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess $values = array(); if (is_array($names)) { - foreach ($this->attributeNames() as $name) { + foreach ($this->attributes() as $name) { if (in_array($name, $names, true)) { $values[$name] = $this->$name; } } } else { - foreach ($this->attributeNames() as $name) { + foreach ($this->attributes() as $name) { $values[$name] = $this->$name; } } @@ -480,13 +498,13 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * @param array $values attribute values (name=>value) to be assigned to the model. * @param boolean $safeOnly whether the assignments should only be done to the safe attributes. * A safe attribute is one that is associated with a validation rule in the current [[scenario]]. - * @see getSafeAttributeNames - * @see attributeNames + * @see safeAttributes() + * @see attributes() */ public function setAttributes($values, $safeOnly = true) { if (is_array($values)) { - $attributes = array_flip($safeOnly ? $this->getSafeAttributeNames() : $this->attributeNames()); + $attributes = array_flip($safeOnly ? $this->safeAttributes() : $this->attributes()); foreach ($values as $name => $value) { if (isset($attributes[$name])) { $this->$name = $value; @@ -517,15 +535,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * Scenario affects how validation is performed and which attributes can * be massively assigned. * - * A validation rule will be performed when calling [[validate()]] - * if its 'on' option is not set or contains the current scenario value. - * - * And an attribute can be massively assigned if it is associated with - * a validation rule for the current scenario. An exception is - * the [[\yii\validators\UnsafeValidator|unsafe]] validator which marks - * the associated attributes as unsafe and not allowed to be massively assigned. - * - * @return string the scenario that this model is in. + * @return string the scenario that this model is in. Defaults to 'default'. */ public function getScenario() { @@ -543,30 +553,35 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** - * Returns the attribute names that are safe to be massively assigned. - * A safe attribute is one that is associated with a validation rule in the current [[scenario]]. + * Returns the attribute names that are safe to be massively assigned in the current scenario. * @return array safe attribute names */ - public function getSafeAttributeNames() + public function safeAttributes() { + $scenarios = $this->scenarios(); $attributes = array(); - $unsafe = array(); - foreach ($this->getActiveValidators() as $validator) { - if (!$validator->safe) { - foreach ($validator->attributes as $name) { - $unsafe[] = $name; - } - } else { - foreach ($validator->attributes as $name) { - $attributes[$name] = true; - } + foreach ($scenarios[$this->getScenario()] as $attribute) { + if ($attribute[0] !== '!') { + $attributes[] = $attribute; } } + return $attributes; + } - foreach ($unsafe as $name) { - unset($attributes[$name]); + /** + * Returns the attribute names that are subject to validation in the current scenario. + * @return array safe attribute names + */ + public function activeAttributes() + { + $scenarios = $this->scenarios(); + $attributes = $scenarios[$this->getScenario()]; + foreach ($attributes as $i => $attribute) { + if ($attribute[0] === '!') { + $attributes[$i] = substr($attribute, 1); + } } - return array_keys($attributes); + return $attributes; } /** diff --git a/framework/db/ar/ActiveRecord.php b/framework/db/ar/ActiveRecord.php index 88c0e5a..2cf76b5 100644 --- a/framework/db/ar/ActiveRecord.php +++ b/framework/db/ar/ActiveRecord.php @@ -588,7 +588,7 @@ abstract class ActiveRecord extends Model * This would return all column names of the table associated with this AR class. * @return array list of attribute names. */ - public function attributeNames() + public function attributes() { return array_keys($this->getMetaData()->table->columns); } @@ -633,7 +633,7 @@ abstract class ActiveRecord extends Model public function getAttributes($names = null) { if ($names === null) { - $names = $this->attributeNames(); + $names = $this->attributes(); } $values = array(); foreach ($names as $name) { @@ -645,7 +645,7 @@ abstract class ActiveRecord extends Model public function getChangedAttributes($names = null) { if ($names === null) { - $names = $this->attributeNames(); + $names = $this->attributes(); } $names = array_flip($names); $attributes = array(); @@ -931,7 +931,7 @@ abstract class ActiveRecord extends Model return false; } if ($attributes === null) { - foreach ($this->attributeNames() as $name) { + foreach ($this->attributes() as $name) { $this->_attributes[$name] = $record->_attributes[$name]; } $this->_oldAttributes = $this->_attributes; diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index ed8d5a4..5dd2fdb 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -42,8 +42,6 @@ namespace yii\validators; * - `captcha`: [[CaptchaValidator]] * - `default`: [[DefaultValueValidator]] * - `exist`: [[ExistValidator]] - * - `safe`: [[SafeValidator]] - * - `unsafe`: [[UnsafeValidator]] * * @author Qiang Xue * @since 2.0 @@ -58,8 +56,6 @@ abstract class Validator extends \yii\base\Component 'match' => '\yii\validators\RegularExpressionValidator', 'email' => '\yii\validators\EmailValidator', 'url' => '\yii\validators\UrlValidator', - 'safe' => '\yii\validators\SafeValidator', - 'unsafe' => '\yii\validators\UnsafeValidator', 'filter' => '\yii\validators\FilterValidator', 'captcha' => '\yii\validators\CaptchaValidator', 'default' => '\yii\validators\DefaultValueValidator', @@ -103,11 +99,6 @@ abstract class Validator extends \yii\base\Component */ public $skipOnError = true; /** - * @var boolean whether attributes listed with this validator should be considered safe for - * massive assignment. Defaults to true. - */ - public $safe = true; - /** * @var boolean whether to enable client-side validation. Defaults to true. * Please refer to [[\yii\web\ActiveForm::enableClientValidation]] for more details about * client-side validation. @@ -187,8 +178,10 @@ abstract class Validator extends \yii\base\Component /** * Validates the specified object. * @param \yii\base\Model $object the data object being validated - * @param array $attributes the list of attributes to be validated. Defaults to null, - * meaning every attribute listed in [[attributes]] will be validated. + * @param array|null $attributes the list of attributes to be validated. + * Note that if an attribute is not associated with the validator, + * it will be ignored. + * If this parameter is null, every attribute listed in [[attributes]] will be validated. */ public function validate($object, $attributes = null) { @@ -228,10 +221,11 @@ abstract class Validator extends \yii\base\Component } /** - * Returns a value indicating whether the validator applies to the specified scenario. - * A validator applies to a scenario as long as any of the following conditions is met: + * Returns a value indicating whether the validator is active for the given scenario and attribute. + * + * A validator is active if * - * - the validator's `on` property is empty + * - the validator's `on` property is empty, or * - the validator's `on` property contains the specified scenario * * @param string $scenario scenario name @@ -239,7 +233,7 @@ abstract class Validator extends \yii\base\Component * the method will also check if the attribute appears in [[attributes]]. * @return boolean whether the validator applies to the specified scenario. */ - public function applyTo($scenario, $attribute = null) + public function isActive($scenario, $attribute = null) { $applies = !isset($this->except[$scenario]) && (empty($this->on) || isset($this->on[$scenario])); return $attribute === null ? $applies : $applies && in_array($attribute, $this->attributes, true); diff --git a/framework/web/Sort.php b/framework/web/Sort.php index 1b6ba63..12e16a5 100644 --- a/framework/web/Sort.php +++ b/framework/web/Sort.php @@ -429,7 +429,7 @@ class CSort extends CComponent $attributes = $this->attributes; } else { if ($this->modelClass !== null) { - $attributes = CActiveRecord::model($this->modelClass)->attributeNames(); + $attributes = CActiveRecord::model($this->modelClass)->attributes(); } else { return false; }