diff --git a/docs/guide/structure-models.md b/docs/guide/structure-models.md index e48a0896a7..9c4cefee55 100644 --- a/docs/guide/structure-models.md +++ b/docs/guide/structure-models.md @@ -392,6 +392,19 @@ have to do it explicitly as follows, $model->secret = $secret; ``` +The same can be done in `rules()` method: + +```php +public function rules() +{ + return [ + [['username', 'password', '!secret'], 'required', 'on' => 'login'] + ]; +} +``` + +In this case attributes `username` and `password` are required, but `secret` must be assigned explicitly. + ## Data Exporting diff --git a/framework/base/Model.php b/framework/base/Model.php index 4d8977f084..03a25f2dd7 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -754,7 +754,7 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab } $attributes = []; foreach ($scenarios[$scenario] as $attribute) { - if ($attribute[0] !== '!') { + if ($attribute[0] !== '!' && !in_array('!' . $attribute, $scenarios[$scenario])) { $attributes[] = $attribute; } } diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index e3a37d4893..c64d64aa68 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -224,17 +224,26 @@ class Validator extends Component * Validates the specified object. * @param \yii\base\Model $model the data model being 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. + * Note that if an attribute is not associated with the validator, or is is prefixed with `!` char - it will be + * ignored. If this parameter is null, every attribute listed in [[attributes]] will be validated. */ public function validateAttributes($model, $attributes = null) { if (is_array($attributes)) { - $attributes = array_intersect($this->attributes, $attributes); + $newAttributes = []; + foreach ($attributes as $attribute) { + if (in_array($attribute, $this->attributes) || in_array('!' . $attribute, $this->attributes)) { + $newAttributes[] = $attribute; + } + } + $attributes = $newAttributes; } else { - $attributes = $this->attributes; + $attributes = []; + foreach ($this->attributes as $attribute) { + $attributes[] = $attribute[0] === '!' ? substr($attribute, 1) : $attribute; + } } + foreach ($attributes as $attribute) { $skip = $this->skipOnError && $model->hasErrors($attribute) || $this->skipOnEmpty && $this->isEmpty($model->$attribute); diff --git a/tests/framework/base/ModelTest.php b/tests/framework/base/ModelTest.php index 72968d196c..a6fa22cdd1 100644 --- a/tests/framework/base/ModelTest.php +++ b/tests/framework/base/ModelTest.php @@ -3,11 +3,11 @@ namespace yiiunit\framework\base; use yii\base\Model; -use yiiunit\data\base\RulesModel; -use yiiunit\TestCase; -use yiiunit\data\base\Speaker; -use yiiunit\data\base\Singer; use yiiunit\data\base\InvalidRulesModel; +use yiiunit\data\base\RulesModel; +use yiiunit\data\base\Singer; +use yiiunit\data\base\Speaker; +use yiiunit\TestCase; /** * @group base @@ -177,6 +177,53 @@ class ModelTest extends TestCase $this->assertEquals(['account_id', 'user_id', 'email', 'name'], $model->activeAttributes()); } + public function testUnsafeAttributes() + { + $model = new RulesModel(); + $model->rules = [ + [['name', '!email'], 'required'], // Name is safe to set, but email is not. Both are required + ]; + $this->assertEquals(['name'], $model->safeAttributes()); + $this->assertEquals(['name', 'email'], $model->activeAttributes()); + $model->attributes = ['name' => 'mdmunir', 'email' => 'mdm@mun.com']; + $this->assertNull($model->email); + $this->assertFalse($model->validate()); + + $model = new RulesModel(); + $model->rules = [ + [['name'], 'required'], + [['!user_id'], 'default', 'value' => '3426'], + ]; + $model->attributes = ['name' => 'mdmunir', 'user_id' => '62792684']; + $this->assertTrue($model->validate()); + $this->assertEquals('3426', $model->user_id); + + $model = new RulesModel(); + $model->rules = [ + [['name', 'email'], 'required'], + [['!email'], 'safe'] + ]; + $this->assertEquals(['name'], $model->safeAttributes()); + $model->attributes = ['name' => 'mdmunir', 'email' => 'm2792684@mdm.com']; + $this->assertFalse($model->validate()); + + $model = new RulesModel(); + $model->rules = [ + [['name', 'email'], 'required'], + [['email'], 'email'], + [['!email'], 'safe', 'on' => 'update'] + ]; + $model->setScenario(RulesModel::SCENARIO_DEFAULT); + $this->assertEquals(['name', 'email'], $model->safeAttributes()); + $model->attributes = ['name' => 'mdmunir', 'email' => 'm2792684@mdm.com']; + $this->assertTrue($model->validate()); + + $model->setScenario('update'); + $this->assertEquals(['name'], $model->safeAttributes()); + $model->attributes = ['name' => 'D426', 'email' => 'd426@mdm.com']; + $this->assertNotEquals('d426@mdm.com', $model->email); + } + public function testErrors() { $speaker = new Speaker(); @@ -344,7 +391,8 @@ class ModelTest extends TestCase public function testCreateValidators() { - $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid validation rule: a rule must specify both attribute names and validator type.'); + $this->setExpectedException('yii\base\InvalidConfigException', + 'Invalid validation rule: a rule must specify both attribute names and validator type.'); $invalid = new InvalidRulesModel(); $invalid->createValidators();