diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index c7bd035c4d..b5bc30296f 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -21,6 +21,7 @@ Yii Framework 2 Change Log - 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) - Bug #8032: `yii\rbac\PhpManager::updateItem()` was unable to rename item updated (ChristopheBrun, 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) @@ -36,7 +37,6 @@ Yii Framework 2 Change Log - Enh #7850: Added `yii\filters\PageCache::cacheCookies` and `cacheHeaders` to allow selectively caching cookies and HTTP headers (qiangxue) - Enh #7867: Implemented findUniqueIndexes for oci and mssql (nineinchnick) - Enh #7912: Added `aria-label` to ActionColumn buttons (LAV45, samdark) -- Enh #7918: `yii\widgets\Pjax` got ability to avoid registering link/form handler via setting `false` to `$linkSelector`/`$formSelector` (usualdesigner, Alex-Code, samdark) - Enh #7973: Added `Schema::getSchemaNames` method (nineinchnick) - Enh: Added `yii\helper\Console::wrapText()` method to wrap indented text by console window width and used it in `yii help` command (cebe) - Enh: Implement batchInsert for oci (nineinchnick) diff --git a/framework/validators/EachValidator.php b/framework/validators/EachValidator.php new file mode 100644 index 0000000000..c044e9fd01 --- /dev/null +++ b/framework/validators/EachValidator.php @@ -0,0 +1,150 @@ + ['trim']], + * ['arrayAttribute', 'each', 'rule' => ['integer']], + * ] + * } + * } + * ~~~ + * + * 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. + * + * @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 = 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..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]] @@ -57,6 +58,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