diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index ad95cde7bc6b..b1612481b97b 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -168,12 +168,14 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup } if (strpos($field, '*') !== false) { - $values = array_filter(array_flatten_with_dots($data), static fn ($key) => preg_match( - '/^' - . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) - . '$/', - $key - ), ARRAY_FILTER_USE_KEY); + $flattenedArray = array_flatten_with_dots($data); + + $values = array_filter( + $flattenedArray, + static fn ($key) => preg_match(self::getRegex($field), $key), + ARRAY_FILTER_USE_KEY + ); + // if keys not found $values = $values ?: [$field => null]; } else { @@ -211,6 +213,20 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup return false; } + /** + * Returns regex pattern for key with dot array syntax. + */ + private static function getRegex(string $field): string + { + return '/\A' + . str_replace( + ['\.\*', '\*\.'], + ['\.[^.]+', '[^.]+\.'], + preg_quote($field, '/') + ) + . '\z/'; + } + /** * Runs the validation process, returning true or false determining whether * validation was successful or not. @@ -814,9 +830,7 @@ private function retrievePlaceholders(string $rule, array $data): array */ public function hasError(string $field): bool { - $pattern = '/^' . str_replace('\.\*', '\..+', preg_quote($field, '/')) . '$/'; - - return (bool) preg_grep($pattern, array_keys($this->getErrors())); + return (bool) preg_grep(self::getRegex($field), array_keys($this->getErrors())); } /** @@ -829,10 +843,11 @@ public function getError(?string $field = null): string $field = array_key_first($this->rules); } - $errors = array_filter($this->getErrors(), static fn ($key) => preg_match( - '/^' . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) . '$/', - $key - ), ARRAY_FILTER_USE_KEY); + $errors = array_filter( + $this->getErrors(), + static fn ($key) => preg_match(self::getRegex($field), $key), + ARRAY_FILTER_USE_KEY + ); return $errors === [] ? '' : implode("\n", $errors); } diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index e015c8b21de9..7cc6f99c40d5 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -1111,9 +1111,9 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void $request = new IncomingRequest($config, new URI(), 'php://input', new UserAgent()); $this->validation->setRules([ - 'id_user.*' => 'numeric', - 'name_user.*' => 'alpha', - 'contacts.*.name' => 'required', + 'id_user.*' => 'numeric', + 'name_user.*' => 'alpha', + 'contacts.friends.*.name' => 'required', ]); $this->validation->withRequest($request->withMethod('post'))->run(); @@ -1121,7 +1121,7 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void 'id_user.0' => 'The id_user.* field must contain only numbers.', 'name_user.0' => 'The name_user.* field may only contain alphabetical characters.', 'name_user.2' => 'The name_user.* field may only contain alphabetical characters.', - 'contacts.friends.0.name' => 'The contacts.*.name field is required.', + 'contacts.friends.0.name' => 'The contacts.friends.*.name field is required.', ], $this->validation->getErrors()); $this->assertSame( @@ -1130,8 +1130,8 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void $this->validation->getError('name_user.*') ); $this->assertSame( - 'The contacts.*.name field is required.', - $this->validation->getError('contacts.*.name') + 'The contacts.friends.*.name field is required.', + $this->validation->getError('contacts.friends.*.name') ); } @@ -1228,17 +1228,17 @@ public function testTranslatedLabelTagReplacement(): void } /** - * @dataProvider provideDotNotationOnIfExistRule + * @dataProvider provideIfExistRuleWithAsterisk * * @see https://github.com/codeigniter4/CodeIgniter4/issues/4521 */ - public function testDotNotationOnIfExistRule(bool $expected, array $rules, array $data): void + public function testIfExistRuleWithAsterisk(bool $expected, array $rules, array $data): void { $actual = $this->validation->setRules($rules)->run($data); $this->assertSame($expected, $actual); } - public static function provideDotNotationOnIfExistRule(): iterable + public static function provideIfExistRuleWithAsterisk(): iterable { yield 'dot-on-end-fail' => [ false, @@ -1613,7 +1613,7 @@ public function testRuleWithLeadingAsterisk(): void /** * @see https://github.com/codeigniter4/CodeIgniter4/issues/5942 */ - public function testRequireWithoutWithWildCard(): void + public function testRequireWithoutWithAsterisk(): void { $data = [ 'a' => [ @@ -1631,4 +1631,50 @@ public function testRequireWithoutWithWildCard(): void $this->validation->getError('a.1.c') ); } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/8128 + */ + public function testRuleWithAsteriskToMultiDimensionalArray(): void + { + $data = [ + 'contacts' => [ + 'name' => 'Joe Smith', + 'just' => [ + 'friends' => [ + [ + 'name' => 'Fred Flinstone', + ], + [ + 'name' => 'Wilma', + ], + ], + ], + ], + ]; + + $this->validation->setRules( + ['contacts.just.friends.*.name' => 'required|max_length[1]'] + ); + $this->assertFalse($this->validation->run($data)); + $this->assertSame( + [ + 'contacts.just.friends.0.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.', + 'contacts.just.friends.1.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.', + ], + $this->validation->getErrors() + ); + + $this->validation->reset(); + $this->validation->setRules( + ['contacts.*.name' => 'required|max_length[1]'] + ); + $this->assertFalse($this->validation->run($data)); + $this->assertSame( + // The data for `contacts.*.name` does not exist. So it is interpreted + // as `null`, and this error message returns. + ['contacts.*.name' => 'The contacts.*.name field is required.'], + $this->validation->getErrors() + ); + } } diff --git a/user_guide_src/source/changelogs/v4.4.4.rst b/user_guide_src/source/changelogs/v4.4.4.rst index 218c56e738d7..cfb0aeb4f121 100644 --- a/user_guide_src/source/changelogs/v4.4.4.rst +++ b/user_guide_src/source/changelogs/v4.4.4.rst @@ -14,6 +14,13 @@ Release Date: Unreleased BREAKING ******** +Validation with Dot Array Syntax +================================ + +A validation rule with the wildcard ``*`` now validates only data in correct +dimensions as "dot array syntax". +See :ref:`Upgrading ` for details. + *************** Message Changes *************** diff --git a/user_guide_src/source/installation/upgrade_444.rst b/user_guide_src/source/installation/upgrade_444.rst index 6dbfa8e7baac..c0b4def6e34d 100644 --- a/user_guide_src/source/installation/upgrade_444.rst +++ b/user_guide_src/source/installation/upgrade_444.rst @@ -20,6 +20,25 @@ Mandatory File Changes Breaking Changes **************** +.. _upgrade-444-validation-with-dot-array-syntax: + +Validation with Dot Array Syntax +================================ + +If you are using :ref:`dot array syntax ` in validation +rules, a bug where ``*`` would validate data in incorrect dimensions has been fixed. + +In previous versions, the rule key ``contacts.*.name`` captured data with any +level like ``contacts.*.name``, ``contacts.*.*.name``, ``contacts.*.*.*.name``, +etc., incorrectly. + +The following code explains details: + +.. literalinclude:: upgrade_444/001.php + :lines: 2- + +If you have code that depends on the bug, fix the the rule key. + ********************* Breaking Enhancements ********************* diff --git a/user_guide_src/source/installation/upgrade_444/001.php b/user_guide_src/source/installation/upgrade_444/001.php new file mode 100644 index 000000000000..9bcc6412141f --- /dev/null +++ b/user_guide_src/source/installation/upgrade_444/001.php @@ -0,0 +1,38 @@ + [ + 'name' => 'Joe Smith', + 'just' => [ + 'friends' => [ + ['name' => 'SATO Taro'], + ['name' => 'Li Ming'], + ['name' => 'Heinz Müller'], + ], + ], + ], +]; + +$validation->setRules( + ['contacts.*.name' => 'required|max_length[8]'] +); + +$validation->run($data); // false + +d($validation->getErrors()); +/* + Before: Captured `contacts.*.*.*.name` incorrectly. + [ + contacts.just.friends.0.name => "The contacts.*.name field cannot exceed 8 characters in length.", + contacts.just.friends.2.name => "The contacts.*.name field cannot exceed 8 characters in length.", + ] + + After: Captures no data for `contacts.*.name`. + [ + contacts.*.name => string (38) "The contacts.*.name field is required.", + ] +*/ diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 0288090b6f6f..8ebec266c0bf 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -314,6 +314,8 @@ To give a labeled error message you can set up as: .. note:: ``setRules()`` will overwrite any rules that were set previously. To add more than one rule to an existing set of rules, use ``setRule()`` multiple times. +.. _validation-dot-array-syntax: + Setting Rules for Array Data ============================ @@ -328,6 +330,10 @@ You can use the ``*`` wildcard symbol to match any one level of the array: .. literalinclude:: validation/010.php :lines: 2- +.. note:: Prior to v4.4.4, due to a bug, the wildcard ``*`` validated data in incorrect + dimensions. See :ref:`Upgrading ` + for details. + "dot array syntax" can also be useful when you have single dimension array data. For example, data returned by multi select dropdown: @@ -591,7 +597,7 @@ If you need to retrieve all error messages for failed fields, you can use the `` If no errors exist, an empty array will be returned. -When using a wildcard, the error will point to a specific field, replacing the asterisk with the appropriate key/keys:: +When using a wildcard (``*``), the error will point to a specific field, replacing the asterisk with the appropriate key/keys:: // for data 'contacts' => [ @@ -606,10 +612,10 @@ When using a wildcard, the error will point to a specific field, replacing the a ] // rule - 'contacts.*.name' => 'required' + 'contacts.friends.*.name' => 'required' // error will be - 'contacts.friends.1.name' => 'The contacts.*.name field is required.' + 'contacts.friends.1.name' => 'The contacts.friends.*.name field is required.' Getting a Single Error ====================== @@ -830,7 +836,8 @@ alpha_numeric_punct No Fails if field contains anything other than alphanumeric, space, or this limited set of punctuation characters: ``~`` (tilde), ``!`` (exclamation), ``#`` (number), - ``$`` (dollar), ``% (percent), & (ampersand), + ``$`` (dollar), ``%`` (percent), + ``&`` (ampersand), ``*`` (asterisk), ``-`` (dash), ``_`` (underscore), ``+`` (plus), ``=`` (equals), ``|`` (vertical bar), diff --git a/user_guide_src/source/libraries/validation/009.php b/user_guide_src/source/libraries/validation/009.php index 7e321e9664ff..85be52b7abb2 100644 --- a/user_guide_src/source/libraries/validation/009.php +++ b/user_guide_src/source/libraries/validation/009.php @@ -4,7 +4,7 @@ * The data to test: * [ * 'contacts' => [ - * 'name' => 'Joe Smith', + * 'name' => 'Joe Smith', * 'friends' => [ * [ * 'name' => 'Fred Flinstone', @@ -21,8 +21,3 @@ $validation->setRules([ 'contacts.name' => 'required|max_length[60]', ]); - -// Fred Flintsone & Wilma -$validation->setRules([ - 'contacts.friends.name' => 'required|max_length[60]', -]); diff --git a/user_guide_src/source/libraries/validation/010.php b/user_guide_src/source/libraries/validation/010.php index 0951f582c882..2646fcc32b99 100644 --- a/user_guide_src/source/libraries/validation/010.php +++ b/user_guide_src/source/libraries/validation/010.php @@ -2,5 +2,5 @@ // Fred Flintsone & Wilma $validation->setRules([ - 'contacts.*.name' => 'required|max_length[60]', + 'contacts.friends.*.name' => 'required|max_length[60]', ]);