Commit da786f65 by Qiang Xue

Implemented new rules and safe attributes

parent fbcf6776
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
namespace yii\base; namespace yii\base;
use yii\util\StringHelper; use yii\util\StringHelper;
use yii\validators\Validator;
use yii\validators\RequiredValidator;
/** /**
* Model is the base class for data models. * Model is the base class for data models.
...@@ -35,7 +37,6 @@ use yii\util\StringHelper; ...@@ -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 $errors Errors for all attributes or the specified attribute. Empty array is returned if no error.
* @property array $attributes Attribute values (name=>value). * @property array $attributes Attribute values (name=>value).
* @property string $scenario The scenario that this model is in. * @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 * @event ModelEvent beforeValidate an event raised at the beginning of [[validate()]]. You may set
* [[ModelEvent::isValid]] to be false to stop the validation. * [[ModelEvent::isValid]] to be false to stop the validation.
...@@ -48,43 +49,8 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess ...@@ -48,43 +49,8 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
{ {
private static $_attributes = array(); // class name => array of attribute names private static $_attributes = array(); // class name => array of attribute names
private $_errors; // attribute name => array of errors private $_errors; // attribute name => array of errors
private $_validators; // validators private $_validators; // Vector of validators
private $_scenario; // scenario private $_scenario = 'default';
/**
* 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;
}
/** /**
* Returns the validation rules for attributes. * Returns the validation rules for attributes.
...@@ -107,7 +73,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess ...@@ -107,7 +73,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
* *
* - attribute list: required, specifies the attributes (separated by commas) to be validated; * - 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 * - 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 * - 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. * 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. * - additional name-value pairs can be specified to initialize the corresponding validator properties.
...@@ -145,6 +111,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess ...@@ -145,6 +111,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
* merge the parent rules with child rules using functions such as `array_merge()`. * merge the parent rules with child rules using functions such as `array_merge()`.
* *
* @return array validation rules * @return array validation rules
* @see scenarios
*/ */
public function rules() public function rules()
{ {
...@@ -152,6 +119,56 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess ...@@ -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. * Returns the attribute labels.
* *
* Attribute labels are mainly used for display purpose. For example, given an attribute * Attribute labels are mainly used for display purpose. For example, given an attribute
...@@ -175,30 +192,33 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess ...@@ -175,30 +192,33 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
/** /**
* Performs the data validation. * Performs the data validation.
* *
* This method executes the validation rules as declared in [[rules()]]. * This method executes the validation rules applicable to the current [[scenario]].
* Only the rules applicable to the current [[scenario]] will be executed. * The following criteria are used to determine whether a rule is currently applicable:
* A rule is considered applicable to a scenario if its `on` option is not set *
* or contains the scenario. * - 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 * This method will call [[beforeValidate()]] and [[afterValidate()]] before and
* after actual validation, respectively. If [[beforeValidate()]] returns false, * after the actual validation, respectively. If [[beforeValidate()]] returns false,
* the validation and [[afterValidate()]] will be cancelled. * 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. * @param array $attributes list of attributes that should be validated.
* If this parameter is empty, it means any attribute listed in the applicable * If this parameter is empty, it means any attribute listed in the applicable
* validation rules should be validated. * validation rules should be validated.
* @param boolean $clearErrors whether to call [[clearErrors()]] before performing validation * @param boolean $clearErrors whether to call [[clearErrors()]] before performing validation
* @return boolean whether the validation is successful without any error. * @return boolean whether the validation is successful without any error.
* @see beforeValidate()
* @see afterValidate()
*/ */
public function validate($attributes = null, $clearErrors = true) public function validate($attributes = null, $clearErrors = true)
{ {
if ($clearErrors) { if ($clearErrors) {
$this->clearErrors(); $this->clearErrors();
} }
if ($attributes === null) {
$attributes = $this->activeAttributes();
}
if ($this->beforeValidate()) { if ($this->beforeValidate()) {
foreach ($this->getActiveValidators() as $validator) { foreach ($this->getActiveValidators() as $validator) {
$validator->validate($this, $attributes); $validator->validate($this, $attributes);
...@@ -214,7 +234,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess ...@@ -214,7 +234,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
* The default implementation raises a `beforeValidate` event. * The default implementation raises a `beforeValidate` event.
* You may override this method to do preliminary checks before validation. * 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. * 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. * If false is returned, the validation will stop and the model is considered invalid.
*/ */
public function beforeValidate() public function beforeValidate()
...@@ -269,8 +289,9 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess ...@@ -269,8 +289,9 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
{ {
$validators = array(); $validators = array();
$scenario = $this->getScenario(); $scenario = $this->getScenario();
/** @var $validator Validator */
foreach ($this->getValidators() as $validator) { foreach ($this->getValidators() as $validator) {
if ($validator->applyTo($scenario, $attribute)) { if ($validator->isActive($scenario, $attribute)) {
$validators[] = $validator; $validators[] = $validator;
} }
} }
...@@ -287,8 +308,10 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess ...@@ -287,8 +308,10 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
{ {
$validators = new Vector; $validators = new Vector;
foreach ($this->rules() as $rule) { foreach ($this->rules() as $rule) {
if (isset($rule[0], $rule[1])) { // attributes, validator type if ($rule instanceof Validator) {
$validator = \yii\validators\Validator::createValidator($rule[1], $this, $rule[0], array_slice($rule, 2)); $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); $validators->add($validator);
} else { } else {
throw new BadConfigException('Invalid validation rule: a rule must specify both attribute names and validator type.'); 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 ...@@ -308,7 +331,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
public function isAttributeRequired($attribute) public function isAttributeRequired($attribute)
{ {
foreach ($this->getActiveValidators($attribute) as $validator) { foreach ($this->getActiveValidators($attribute) as $validator) {
if ($validator instanceof \yii\validators\RequiredValidator) { if ($validator instanceof RequiredValidator) {
return true; return true;
} }
} }
...@@ -322,13 +345,8 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess ...@@ -322,13 +345,8 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
*/ */
public function isAttributeSafe($attribute) public function isAttributeSafe($attribute)
{ {
$validators = $this->getActiveValidators($attribute); $scenarios = $this->scenarios();
foreach ($validators as $validator) { return in_array($attribute, $scenarios[$this->getScenario()], true);
if (!$validator->safe) {
return false;
}
}
return $validators !== array();
} }
/** /**
...@@ -346,7 +364,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess ...@@ -346,7 +364,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
/** /**
* Returns a value indicating whether there is any validation error. * 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. * @return boolean whether there is any error.
*/ */
public function hasErrors($attribute = null) public function hasErrors($attribute = null)
...@@ -452,7 +470,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess ...@@ -452,7 +470,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
/** /**
* Returns attribute values. * Returns attribute values.
* @param array $names list of attributes whose value needs to be returned. * @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. * If it is an array, only the attributes in the array will be returned.
* @return array attribute values (name=>value). * @return array attribute values (name=>value).
*/ */
...@@ -461,13 +479,13 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess ...@@ -461,13 +479,13 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
$values = array(); $values = array();
if (is_array($names)) { if (is_array($names)) {
foreach ($this->attributeNames() as $name) { foreach ($this->attributes() as $name) {
if (in_array($name, $names, true)) { if (in_array($name, $names, true)) {
$values[$name] = $this->$name; $values[$name] = $this->$name;
} }
} }
} else { } else {
foreach ($this->attributeNames() as $name) { foreach ($this->attributes() as $name) {
$values[$name] = $this->$name; $values[$name] = $this->$name;
} }
} }
...@@ -480,13 +498,13 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess ...@@ -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 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. * @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]]. * A safe attribute is one that is associated with a validation rule in the current [[scenario]].
* @see getSafeAttributeNames * @see safeAttributes()
* @see attributeNames * @see attributes()
*/ */
public function setAttributes($values, $safeOnly = true) public function setAttributes($values, $safeOnly = true)
{ {
if (is_array($values)) { 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) { foreach ($values as $name => $value) {
if (isset($attributes[$name])) { if (isset($attributes[$name])) {
$this->$name = $value; $this->$name = $value;
...@@ -517,15 +535,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess ...@@ -517,15 +535,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
* Scenario affects how validation is performed and which attributes can * Scenario affects how validation is performed and which attributes can
* be massively assigned. * be massively assigned.
* *
* A validation rule will be performed when calling [[validate()]] * @return string the scenario that this model is in. Defaults to 'default'.
* 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.
*/ */
public function getScenario() public function getScenario()
{ {
...@@ -543,30 +553,35 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess ...@@ -543,30 +553,35 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
} }
/** /**
* Returns the attribute names that are safe to be massively assigned. * Returns the attribute names that are safe to be massively assigned in the current scenario.
* A safe attribute is one that is associated with a validation rule in the current [[scenario]].
* @return array safe attribute names * @return array safe attribute names
*/ */
public function getSafeAttributeNames() public function safeAttributes()
{ {
$scenarios = $this->scenarios();
$attributes = array(); $attributes = array();
$unsafe = array(); foreach ($scenarios[$this->getScenario()] as $attribute) {
foreach ($this->getActiveValidators() as $validator) { if ($attribute[0] !== '!') {
if (!$validator->safe) { $attributes[] = $attribute;
foreach ($validator->attributes as $name) {
$unsafe[] = $name;
}
} else {
foreach ($validator->attributes as $name) {
$attributes[$name] = true;
} }
} }
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;
} }
/** /**
......
...@@ -588,7 +588,7 @@ abstract class ActiveRecord extends Model ...@@ -588,7 +588,7 @@ abstract class ActiveRecord extends Model
* This would return all column names of the table associated with this AR class. * This would return all column names of the table associated with this AR class.
* @return array list of attribute names. * @return array list of attribute names.
*/ */
public function attributeNames() public function attributes()
{ {
return array_keys($this->getMetaData()->table->columns); return array_keys($this->getMetaData()->table->columns);
} }
...@@ -633,7 +633,7 @@ abstract class ActiveRecord extends Model ...@@ -633,7 +633,7 @@ abstract class ActiveRecord extends Model
public function getAttributes($names = null) public function getAttributes($names = null)
{ {
if ($names === null) { if ($names === null) {
$names = $this->attributeNames(); $names = $this->attributes();
} }
$values = array(); $values = array();
foreach ($names as $name) { foreach ($names as $name) {
...@@ -645,7 +645,7 @@ abstract class ActiveRecord extends Model ...@@ -645,7 +645,7 @@ abstract class ActiveRecord extends Model
public function getChangedAttributes($names = null) public function getChangedAttributes($names = null)
{ {
if ($names === null) { if ($names === null) {
$names = $this->attributeNames(); $names = $this->attributes();
} }
$names = array_flip($names); $names = array_flip($names);
$attributes = array(); $attributes = array();
...@@ -931,7 +931,7 @@ abstract class ActiveRecord extends Model ...@@ -931,7 +931,7 @@ abstract class ActiveRecord extends Model
return false; return false;
} }
if ($attributes === null) { if ($attributes === null) {
foreach ($this->attributeNames() as $name) { foreach ($this->attributes() as $name) {
$this->_attributes[$name] = $record->_attributes[$name]; $this->_attributes[$name] = $record->_attributes[$name];
} }
$this->_oldAttributes = $this->_attributes; $this->_oldAttributes = $this->_attributes;
......
...@@ -42,8 +42,6 @@ namespace yii\validators; ...@@ -42,8 +42,6 @@ namespace yii\validators;
* - `captcha`: [[CaptchaValidator]] * - `captcha`: [[CaptchaValidator]]
* - `default`: [[DefaultValueValidator]] * - `default`: [[DefaultValueValidator]]
* - `exist`: [[ExistValidator]] * - `exist`: [[ExistValidator]]
* - `safe`: [[SafeValidator]]
* - `unsafe`: [[UnsafeValidator]]
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
...@@ -58,8 +56,6 @@ abstract class Validator extends \yii\base\Component ...@@ -58,8 +56,6 @@ abstract class Validator extends \yii\base\Component
'match' => '\yii\validators\RegularExpressionValidator', 'match' => '\yii\validators\RegularExpressionValidator',
'email' => '\yii\validators\EmailValidator', 'email' => '\yii\validators\EmailValidator',
'url' => '\yii\validators\UrlValidator', 'url' => '\yii\validators\UrlValidator',
'safe' => '\yii\validators\SafeValidator',
'unsafe' => '\yii\validators\UnsafeValidator',
'filter' => '\yii\validators\FilterValidator', 'filter' => '\yii\validators\FilterValidator',
'captcha' => '\yii\validators\CaptchaValidator', 'captcha' => '\yii\validators\CaptchaValidator',
'default' => '\yii\validators\DefaultValueValidator', 'default' => '\yii\validators\DefaultValueValidator',
...@@ -103,11 +99,6 @@ abstract class Validator extends \yii\base\Component ...@@ -103,11 +99,6 @@ abstract class Validator extends \yii\base\Component
*/ */
public $skipOnError = true; 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. * @var boolean whether to enable client-side validation. Defaults to true.
* Please refer to [[\yii\web\ActiveForm::enableClientValidation]] for more details about * Please refer to [[\yii\web\ActiveForm::enableClientValidation]] for more details about
* client-side validation. * client-side validation.
...@@ -187,8 +178,10 @@ abstract class Validator extends \yii\base\Component ...@@ -187,8 +178,10 @@ abstract class Validator extends \yii\base\Component
/** /**
* Validates the specified object. * Validates the specified object.
* @param \yii\base\Model $object the data object being validated * @param \yii\base\Model $object the data object being validated
* @param array $attributes the list of attributes to be validated. Defaults to null, * @param array|null $attributes the list of attributes to be validated.
* meaning every attribute listed in [[attributes]] will 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) public function validate($object, $attributes = null)
{ {
...@@ -228,10 +221,11 @@ abstract class Validator extends \yii\base\Component ...@@ -228,10 +221,11 @@ abstract class Validator extends \yii\base\Component
} }
/** /**
* Returns a value indicating whether the validator applies to the specified scenario. * Returns a value indicating whether the validator is active for the given scenario and attribute.
* A validator applies to a scenario as long as any of the following conditions is met: *
* 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 * - the validator's `on` property contains the specified scenario
* *
* @param string $scenario scenario name * @param string $scenario scenario name
...@@ -239,7 +233,7 @@ abstract class Validator extends \yii\base\Component ...@@ -239,7 +233,7 @@ abstract class Validator extends \yii\base\Component
* the method will also check if the attribute appears in [[attributes]]. * the method will also check if the attribute appears in [[attributes]].
* @return boolean whether the validator applies to the specified scenario. * @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])); $applies = !isset($this->except[$scenario]) && (empty($this->on) || isset($this->on[$scenario]));
return $attribute === null ? $applies : $applies && in_array($attribute, $this->attributes, true); return $attribute === null ? $applies : $applies && in_array($attribute, $this->attributes, true);
......
...@@ -429,7 +429,7 @@ class CSort extends CComponent ...@@ -429,7 +429,7 @@ class CSort extends CComponent
$attributes = $this->attributes; $attributes = $this->attributes;
} else { } else {
if ($this->modelClass !== null) { if ($this->modelClass !== null) {
$attributes = CActiveRecord::model($this->modelClass)->attributeNames(); $attributes = CActiveRecord::model($this->modelClass)->attributes();
} else { } else {
return false; return false;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment