From 35eb833a2b9f2990862046257b24168eac0a8b5a Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Thu, 9 Apr 2015 16:48:15 +0300 Subject: [PATCH 1/4] Added `yii\validators\EachValidator` --- framework/CHANGELOG.md | 1 + framework/validators/EachValidator.php | 148 ++++++++++++++++++ framework/validators/Validator.php | 1 + .../validators/EachValidatorTest.php | 75 +++++++++ 4 files changed, 225 insertions(+) create mode 100644 framework/validators/EachValidator.php create mode 100644 tests/unit/framework/validators/EachValidatorTest.php diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 91a662d8ea..bb5d84f3ea 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -20,6 +20,7 @@ Yii Framework 2 Change Log - Bug #7957: Removed extra `parseFloat()` call for the `compare` js validator (CthulhuDen) - Bug #8012: Fixed fetching multiple relations between two tables for pgsql (nineinchnick) - Bug #8014: Fixed setting incorrect form "action" property after submitting a form using a link with "data-method" and containing "action" among "data-params" (samdark) +- Enh #3376: Added `yii\validators\EachValidator`, which allows validation of the array attributes (klimov-paul) - Enh #6895: Added `ignoreCategories` config option for message command to ignore categories specified (samdark) - Enh #6975: Pressing arrows while focused in inputs of Active Form with `validateOnType` enabled no longer triggers validation (slinstj) - Enh #7409: Allow `yii\filters\auth\CompositeAuth::authMethods` to take authentication objects (fernandezekiel, qiangxue) diff --git a/framework/validators/EachValidator.php b/framework/validators/EachValidator.php new file mode 100644 index 0000000000..d264a01a42 --- /dev/null +++ b/framework/validators/EachValidator.php @@ -0,0 +1,148 @@ + ['trim']], + * ['arrayAttribute', 'each', 'rule' => ['integer']], + * ] + * } + * } + * ~~~ + * + * Note: this validator will not work with validation declared via model inline method. + * + * @property Validator $validator related validator instance. This property is read only. + * + * @author Paul Klimov + * @since 2.0.4 + */ +class EachValidator extends Validator +{ + /** + * @var array|Validator definition of the validation rule, which should be used on array values. + * It should be specified in the same format as at [[yii\base\Model::rules()]], except it should not + * contain attribute list as the first element. + * For example: + * + * ~~~ + * ['integer'] + * ['match', 'pattern' => '/[a-z]/is'] + * ~~~ + * + * Please refer to [[yii\base\Model::rules()]] for more details. + */ + public $rule; + /** + * @var boolean whether to use error message composed by validator declared via [[rule]] if its validation fails. + * If enabled, error message specified for this validator itself will appear only if attribute value is not an array. + * If disabled, own error message value will be used always. + */ + public $allowMessageFromRule = true; + + /** + * @var Validator validator instance. + */ + private $_validator; + + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = $this->allowMessageFromRule ? Yii::t('yii', '{attribute} should be an array.') : Yii::t('yii', '{attribute} is invalid.'); + } + } + + /** + * Returns the validator declared in [[rule]]. + * @return Validator the declared validator. + */ + public function getValidator() + { + if ($this->_validator === null) { + $this->_validator = $this->createValidators(); + } + return $this->_validator; + } + + /** + * Creates validator object based on the validation rule specified in [[rule]]. + * @return Validator validator instance + * @throws InvalidConfigException if any validation rule configuration is invalid + */ + private function createValidators() + { + $rule = $this->rule; + if ($rule instanceof Validator) { + return $rule; + } elseif (is_array($rule) && isset($rule[0])) { // validator type + return Validator::createValidator($rule[0], new Model(), $this->attributes, array_slice($rule, 1)); + } else { + throw new InvalidConfigException('Invalid validation rule: a rule must be an array specifying validator type.'); + } + } + + /** + * @inheritdoc + */ + public function validateAttribute($model, $attribute) + { + $value = $model->$attribute; + $validator = $this->getValidator(); + if ($validator instanceof FilterValidator && is_array($value)) { + $filteredValue = []; + foreach ($value as $k => $v) { + if (!$validator->skipOnArray || !is_array($v)) { + $filteredValue[$k] = call_user_func($validator->filter, $v); + } + } + $model->$attribute = $filteredValue; + } else { + parent::validateAttribute($model, $attribute); + } + } + + /** + * @inheritdoc + */ + protected function validateValue($value) + { + if (!is_array($value)) { + return [$this->message, []]; + } + + $validator = $this->getValidator(); + foreach ($value as $v) { + $result = $validator->validateValue($v); + if ($result !== null) { + return $this->allowMessageFromRule ? $result : [$this->message, []]; + } + } + + return null; + } +} \ No newline at end of file diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index f74e00c020..b2a140534d 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -57,6 +57,7 @@ class Validator extends Component 'date' => 'yii\validators\DateValidator', 'default' => 'yii\validators\DefaultValueValidator', 'double' => 'yii\validators\NumberValidator', + 'each' => 'yii\validators\EachValidator', 'email' => 'yii\validators\EmailValidator', 'exist' => 'yii\validators\ExistValidator', 'file' => 'yii\validators\FileValidator', diff --git a/tests/unit/framework/validators/EachValidatorTest.php b/tests/unit/framework/validators/EachValidatorTest.php new file mode 100644 index 0000000000..3a50e59fa5 --- /dev/null +++ b/tests/unit/framework/validators/EachValidatorTest.php @@ -0,0 +1,75 @@ +mockApplication(); + } + + public function testArrayFormat() + { + $validator = new EachValidator(['rule' => ['required']]); + + $this->assertFalse($validator->validate('not array')); + $this->assertTrue($validator->validate(['value'])); + } + + /** + * @depends testArrayFormat + */ + public function testValidate() + { + $validator = new EachValidator(['rule' => ['integer']]); + + $this->assertTrue($validator->validate([1, 3, 8])); + $this->assertFalse($validator->validate([1, 'text', 8])); + } + + /** + * @depends testArrayFormat + */ + public function testFilter() + { + $model = FakedValidationModel::createWithAttributes([ + 'attr_one' => [ + ' to be trimmed ' + ], + ]); + $validator = new EachValidator(['rule' => ['trim']]); + $validator->validateAttribute($model, 'attr_one'); + $this->assertEquals('to be trimmed', $model->attr_one[0]); + } + + /** + * @depends testValidate + */ + public function testAllowMessageFromRule() + { + $model = FakedValidationModel::createWithAttributes([ + 'attr_one' => [ + 'text' + ], + ]); + $validator = new EachValidator(['rule' => ['integer']]); + + $validator->allowMessageFromRule = true; + $validator->validateAttribute($model, 'attr_one'); + $this->assertContains('integer', $model->getFirstError('attr_one')); + + $model->clearErrors(); + $validator->allowMessageFromRule = false; + $validator->validateAttribute($model, 'attr_one'); + $this->assertNotContains('integer', $model->getFirstError('attr_one')); + } +} \ No newline at end of file From 9fedb978f610f53f908e30bcfaab63f76193d2df Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Thu, 9 Apr 2015 18:13:50 +0300 Subject: [PATCH 2/4] Docs for `yii\validators\EachValidator` adjusted --- framework/validators/EachValidator.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/framework/validators/EachValidator.php b/framework/validators/EachValidator.php index d264a01a42..69a66c5a29 100644 --- a/framework/validators/EachValidator.php +++ b/framework/validators/EachValidator.php @@ -30,7 +30,9 @@ use yii\base\Model; * } * ~~~ * - * Note: this validator will not work with validation declared via model inline method. + * Note: this validator will not work with validation declared via model inline method. If you declare inline + * validation rule for attribute, you should avoid usage of this validator and iterate over array attribute + * values manually inside your code. * * @property Validator $validator related validator instance. This property is read only. * From e29fcc8ea2c4113ab34f38c3dcf5f23027574537 Mon Sep 17 00:00:00 2001 From: Klimov Paul Date: Thu, 9 Apr 2015 18:14:35 +0300 Subject: [PATCH 3/4] Docs about `yii\validators\EachValidator` adjusted --- framework/validators/Validator.php | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index b2a140534d..63af9b7465 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -27,6 +27,7 @@ use yii\base\NotSupportedException; * - `date`: [[DateValidator]] * - `default`: [[DefaultValueValidator]] * - `double`: [[NumberValidator]] + * - `each`: [[EachValidator]] * - `email`: [[EmailValidator]] * - `exist`: [[ExistValidator]] * - `file`: [[FileValidator]] From d594ab0b03690b8f4f27392de5075429d29aaf31 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Sat, 11 Apr 2015 13:59:02 +0300 Subject: [PATCH 4/4] `yii\validators\EachValidator` uses single generic error message --- framework/validators/EachValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/validators/EachValidator.php b/framework/validators/EachValidator.php index 69a66c5a29..c044e9fd01 100644 --- a/framework/validators/EachValidator.php +++ b/framework/validators/EachValidator.php @@ -75,7 +75,7 @@ class EachValidator extends Validator { parent::init(); if ($this->message === null) { - $this->message = $this->allowMessageFromRule ? Yii::t('yii', '{attribute} should be an array.') : Yii::t('yii', '{attribute} is invalid.'); + $this->message = Yii::t('yii', '{attribute} is invalid.'); } }