Skip to content

Commit 3266127

Browse files
authored
Merge pull request #8129 from kenjis/fix-validation-dot-array-syntax
fix: Validation rule with `*` gets incorrect values as dot array syntax
2 parents 37af29e + 4913acc commit 3266127

File tree

8 files changed

+161
-34
lines changed

8 files changed

+161
-34
lines changed

system/Validation/Validation.php

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,14 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
168168
}
169169

170170
if (strpos($field, '*') !== false) {
171-
$values = array_filter(array_flatten_with_dots($data), static fn ($key) => preg_match(
172-
'/^'
173-
. str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/'))
174-
. '$/',
175-
$key
176-
), ARRAY_FILTER_USE_KEY);
171+
$flattenedArray = array_flatten_with_dots($data);
172+
173+
$values = array_filter(
174+
$flattenedArray,
175+
static fn ($key) => preg_match(self::getRegex($field), $key),
176+
ARRAY_FILTER_USE_KEY
177+
);
178+
177179
// if keys not found
178180
$values = $values ?: [$field => null];
179181
} else {
@@ -211,6 +213,20 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
211213
return false;
212214
}
213215

216+
/**
217+
* Returns regex pattern for key with dot array syntax.
218+
*/
219+
private static function getRegex(string $field): string
220+
{
221+
return '/\A'
222+
. str_replace(
223+
['\.\*', '\*\.'],
224+
['\.[^.]+', '[^.]+\.'],
225+
preg_quote($field, '/')
226+
)
227+
. '\z/';
228+
}
229+
214230
/**
215231
* Runs the validation process, returning true or false determining whether
216232
* validation was successful or not.
@@ -814,9 +830,7 @@ private function retrievePlaceholders(string $rule, array $data): array
814830
*/
815831
public function hasError(string $field): bool
816832
{
817-
$pattern = '/^' . str_replace('\.\*', '\..+', preg_quote($field, '/')) . '$/';
818-
819-
return (bool) preg_grep($pattern, array_keys($this->getErrors()));
833+
return (bool) preg_grep(self::getRegex($field), array_keys($this->getErrors()));
820834
}
821835

822836
/**
@@ -829,10 +843,11 @@ public function getError(?string $field = null): string
829843
$field = array_key_first($this->rules);
830844
}
831845

832-
$errors = array_filter($this->getErrors(), static fn ($key) => preg_match(
833-
'/^' . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) . '$/',
834-
$key
835-
), ARRAY_FILTER_USE_KEY);
846+
$errors = array_filter(
847+
$this->getErrors(),
848+
static fn ($key) => preg_match(self::getRegex($field), $key),
849+
ARRAY_FILTER_USE_KEY
850+
);
836851

837852
return $errors === [] ? '' : implode("\n", $errors);
838853
}

tests/system/Validation/ValidationTest.php

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,17 +1111,17 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
11111111
$request = new IncomingRequest($config, new URI(), 'php://input', new UserAgent());
11121112

11131113
$this->validation->setRules([
1114-
'id_user.*' => 'numeric',
1115-
'name_user.*' => 'alpha',
1116-
'contacts.*.name' => 'required',
1114+
'id_user.*' => 'numeric',
1115+
'name_user.*' => 'alpha',
1116+
'contacts.friends.*.name' => 'required',
11171117
]);
11181118

11191119
$this->validation->withRequest($request->withMethod('post'))->run();
11201120
$this->assertSame([
11211121
'id_user.0' => 'The id_user.* field must contain only numbers.',
11221122
'name_user.0' => 'The name_user.* field may only contain alphabetical characters.',
11231123
'name_user.2' => 'The name_user.* field may only contain alphabetical characters.',
1124-
'contacts.friends.0.name' => 'The contacts.*.name field is required.',
1124+
'contacts.friends.0.name' => 'The contacts.friends.*.name field is required.',
11251125
], $this->validation->getErrors());
11261126

11271127
$this->assertSame(
@@ -1130,8 +1130,8 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
11301130
$this->validation->getError('name_user.*')
11311131
);
11321132
$this->assertSame(
1133-
'The contacts.*.name field is required.',
1134-
$this->validation->getError('contacts.*.name')
1133+
'The contacts.friends.*.name field is required.',
1134+
$this->validation->getError('contacts.friends.*.name')
11351135
);
11361136
}
11371137

@@ -1228,17 +1228,17 @@ public function testTranslatedLabelTagReplacement(): void
12281228
}
12291229

12301230
/**
1231-
* @dataProvider provideDotNotationOnIfExistRule
1231+
* @dataProvider provideIfExistRuleWithAsterisk
12321232
*
12331233
* @see https://github.com/codeigniter4/CodeIgniter4/issues/4521
12341234
*/
1235-
public function testDotNotationOnIfExistRule(bool $expected, array $rules, array $data): void
1235+
public function testIfExistRuleWithAsterisk(bool $expected, array $rules, array $data): void
12361236
{
12371237
$actual = $this->validation->setRules($rules)->run($data);
12381238
$this->assertSame($expected, $actual);
12391239
}
12401240

1241-
public static function provideDotNotationOnIfExistRule(): iterable
1241+
public static function provideIfExistRuleWithAsterisk(): iterable
12421242
{
12431243
yield 'dot-on-end-fail' => [
12441244
false,
@@ -1613,7 +1613,7 @@ public function testRuleWithLeadingAsterisk(): void
16131613
/**
16141614
* @see https://github.com/codeigniter4/CodeIgniter4/issues/5942
16151615
*/
1616-
public function testRequireWithoutWithWildCard(): void
1616+
public function testRequireWithoutWithAsterisk(): void
16171617
{
16181618
$data = [
16191619
'a' => [
@@ -1631,4 +1631,50 @@ public function testRequireWithoutWithWildCard(): void
16311631
$this->validation->getError('a.1.c')
16321632
);
16331633
}
1634+
1635+
/**
1636+
* @see https://github.com/codeigniter4/CodeIgniter4/issues/8128
1637+
*/
1638+
public function testRuleWithAsteriskToMultiDimensionalArray(): void
1639+
{
1640+
$data = [
1641+
'contacts' => [
1642+
'name' => 'Joe Smith',
1643+
'just' => [
1644+
'friends' => [
1645+
[
1646+
'name' => 'Fred Flinstone',
1647+
],
1648+
[
1649+
'name' => 'Wilma',
1650+
],
1651+
],
1652+
],
1653+
],
1654+
];
1655+
1656+
$this->validation->setRules(
1657+
['contacts.just.friends.*.name' => 'required|max_length[1]']
1658+
);
1659+
$this->assertFalse($this->validation->run($data));
1660+
$this->assertSame(
1661+
[
1662+
'contacts.just.friends.0.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
1663+
'contacts.just.friends.1.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
1664+
],
1665+
$this->validation->getErrors()
1666+
);
1667+
1668+
$this->validation->reset();
1669+
$this->validation->setRules(
1670+
['contacts.*.name' => 'required|max_length[1]']
1671+
);
1672+
$this->assertFalse($this->validation->run($data));
1673+
$this->assertSame(
1674+
// The data for `contacts.*.name` does not exist. So it is interpreted
1675+
// as `null`, and this error message returns.
1676+
['contacts.*.name' => 'The contacts.*.name field is required.'],
1677+
$this->validation->getErrors()
1678+
);
1679+
}
16341680
}

user_guide_src/source/changelogs/v4.4.4.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ Release Date: Unreleased
1414
BREAKING
1515
********
1616

17+
Validation with Dot Array Syntax
18+
================================
19+
20+
A validation rule with the wildcard ``*`` now validates only data in correct
21+
dimensions as "dot array syntax".
22+
See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>` for details.
23+
1724
***************
1825
Message Changes
1926
***************

user_guide_src/source/installation/upgrade_444.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,25 @@ Mandatory File Changes
2020
Breaking Changes
2121
****************
2222

23+
.. _upgrade-444-validation-with-dot-array-syntax:
24+
25+
Validation with Dot Array Syntax
26+
================================
27+
28+
If you are using :ref:`dot array syntax <validation-dot-array-syntax>` in validation
29+
rules, a bug where ``*`` would validate data in incorrect dimensions has been fixed.
30+
31+
In previous versions, the rule key ``contacts.*.name`` captured data with any
32+
level like ``contacts.*.name``, ``contacts.*.*.name``, ``contacts.*.*.*.name``,
33+
etc., incorrectly.
34+
35+
The following code explains details:
36+
37+
.. literalinclude:: upgrade_444/001.php
38+
:lines: 2-
39+
40+
If you have code that depends on the bug, fix the the rule key.
41+
2342
*********************
2443
Breaking Enhancements
2544
*********************
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
use Config\Services;
4+
5+
$validation = Services::validation();
6+
7+
$data = [
8+
'contacts' => [
9+
'name' => 'Joe Smith',
10+
'just' => [
11+
'friends' => [
12+
['name' => 'SATO Taro'],
13+
['name' => 'Li Ming'],
14+
['name' => 'Heinz Müller'],
15+
],
16+
],
17+
],
18+
];
19+
20+
$validation->setRules(
21+
['contacts.*.name' => 'required|max_length[8]']
22+
);
23+
24+
$validation->run($data); // false
25+
26+
d($validation->getErrors());
27+
/*
28+
Before: Captured `contacts.*.*.*.name` incorrectly.
29+
[
30+
contacts.just.friends.0.name => "The contacts.*.name field cannot exceed 8 characters in length.",
31+
contacts.just.friends.2.name => "The contacts.*.name field cannot exceed 8 characters in length.",
32+
]
33+
34+
After: Captures no data for `contacts.*.name`.
35+
[
36+
contacts.*.name => string (38) "The contacts.*.name field is required.",
37+
]
38+
*/

user_guide_src/source/libraries/validation.rst

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,8 @@ To give a labeled error message you can set up as:
314314
.. note:: ``setRules()`` will overwrite any rules that were set previously. To add more than one
315315
rule to an existing set of rules, use ``setRule()`` multiple times.
316316

317+
.. _validation-dot-array-syntax:
318+
317319
Setting Rules for Array Data
318320
============================
319321

@@ -328,6 +330,10 @@ You can use the ``*`` wildcard symbol to match any one level of the array:
328330
.. literalinclude:: validation/010.php
329331
:lines: 2-
330332

333+
.. note:: Prior to v4.4.4, due to a bug, the wildcard ``*`` validated data in incorrect
334+
dimensions. See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>`
335+
for details.
336+
331337
"dot array syntax" can also be useful when you have single dimension array data.
332338
For example, data returned by multi select dropdown:
333339

@@ -591,7 +597,7 @@ If you need to retrieve all error messages for failed fields, you can use the ``
591597

592598
If no errors exist, an empty array will be returned.
593599

594-
When using a wildcard, the error will point to a specific field, replacing the asterisk with the appropriate key/keys::
600+
When using a wildcard (``*``), the error will point to a specific field, replacing the asterisk with the appropriate key/keys::
595601

596602
// for data
597603
'contacts' => [
@@ -606,10 +612,10 @@ When using a wildcard, the error will point to a specific field, replacing the a
606612
]
607613

608614
// rule
609-
'contacts.*.name' => 'required'
615+
'contacts.friends.*.name' => 'required'
610616

611617
// error will be
612-
'contacts.friends.1.name' => 'The contacts.*.name field is required.'
618+
'contacts.friends.1.name' => 'The contacts.friends.*.name field is required.'
613619

614620
Getting a Single Error
615621
======================
@@ -830,7 +836,8 @@ alpha_numeric_punct No Fails if field contains anything other than
830836
alphanumeric, space, or this limited set of
831837
punctuation characters: ``~`` (tilde),
832838
``!`` (exclamation), ``#`` (number),
833-
``$`` (dollar), ``% (percent), & (ampersand),
839+
``$`` (dollar), ``%`` (percent),
840+
``&`` (ampersand),
834841
``*`` (asterisk), ``-`` (dash),
835842
``_`` (underscore), ``+`` (plus),
836843
``=`` (equals), ``|`` (vertical bar),

user_guide_src/source/libraries/validation/009.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* The data to test:
55
* [
66
* 'contacts' => [
7-
* 'name' => 'Joe Smith',
7+
* 'name' => 'Joe Smith',
88
* 'friends' => [
99
* [
1010
* 'name' => 'Fred Flinstone',
@@ -21,8 +21,3 @@
2121
$validation->setRules([
2222
'contacts.name' => 'required|max_length[60]',
2323
]);
24-
25-
// Fred Flintsone & Wilma
26-
$validation->setRules([
27-
'contacts.friends.name' => 'required|max_length[60]',
28-
]);

user_guide_src/source/libraries/validation/010.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
// Fred Flintsone & Wilma
44
$validation->setRules([
5-
'contacts.*.name' => 'required|max_length[60]',
5+
'contacts.friends.*.name' => 'required|max_length[60]',
66
]);

0 commit comments

Comments
 (0)