Commit da786f65 by Qiang Xue

Implemented new rules and safe attributes

parent fbcf6776
......@@ -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;
}
/**
......
......@@ -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;
......
......@@ -42,8 +42,6 @@ namespace yii\validators;
* - `captcha`: [[CaptchaValidator]]
* - `default`: [[DefaultValueValidator]]
* - `exist`: [[ExistValidator]]
* - `safe`: [[SafeValidator]]
* - `unsafe`: [[UnsafeValidator]]
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @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);
......
......@@ -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;
}
......
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