Skip to content

Commit 003bdf2

Browse files
committed
refactor
1 parent e7785e6 commit 003bdf2

File tree

3 files changed

+65
-55
lines changed

3 files changed

+65
-55
lines changed

system/Validation/Validation.php

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -178,14 +178,13 @@ public function run(?array $data = null, ?string $group = null, $dbGroup = null)
178178
ARRAY_FILTER_USE_KEY,
179179
);
180180

181-
// For required* rules: when at least one sibling path already
182-
// matched (partial-missing scenario), also emit null for array
183-
// elements that are structurally present but lack the leaf key,
184-
// so that the required rule can fire for each of them.
185-
if ($values !== [] && $this->rulesHaveRequired($rules)) {
186-
foreach ($this->walkForAllPossiblePaths(explode('.', $field), $data, '') as $path) {
187-
$values[$path] = null;
188-
}
181+
// Emit null for every leaf path that is structurally reachable
182+
// but whose key is absent from the data. This mirrors the
183+
// non-wildcard behaviour where a missing key is treated as null,
184+
// so that all rules behave consistently regardless of whether
185+
// the field uses a wildcard or not.
186+
foreach ($this->walkForAllPossiblePaths(explode('.', $field), $data, '') as $path) {
187+
$values[$path] = null;
189188
}
190189

191190
// if keys not found
@@ -997,30 +996,6 @@ protected function splitRules(string $rules): array
997996
return array_unique($rules);
998997
}
999998

1000-
/**
1001-
* Returns true if any rule in the set is required, required_with, or required_without.
1002-
* Used to decide whether to emit null for missing wildcard leaf keys.
1003-
*
1004-
* @param list<string> $rules
1005-
*/
1006-
private function rulesHaveRequired(array $rules): bool
1007-
{
1008-
foreach ($rules as $rule) {
1009-
if (! is_string($rule)) {
1010-
continue;
1011-
}
1012-
1013-
$ruleName = strstr($rule, '[', true);
1014-
$name = $ruleName !== false ? $ruleName : $rule;
1015-
1016-
if (in_array($name, ['required', 'required_with', 'required_without'], true)) {
1017-
return true;
1018-
}
1019-
}
1020-
1021-
return false;
1022-
}
1023-
1024999
/**
10251000
* Entry point: allocates a single accumulator and delegates to the
10261001
* recursive collector, so no intermediate arrays are built or unpacked.

tests/system/Validation/ValidationTest.php

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1850,9 +1850,9 @@ public function testRuleWithAsteriskToMultiDimensionalArray(): void
18501850
);
18511851
$this->assertFalse($this->validation->run($data));
18521852
$this->assertSame(
1853-
// The data for `contacts.*.name` does not exist. So it is interpreted
1854-
// as `null`, and this error message returns.
1855-
['contacts.*.name' => 'The contacts.*.name field is required.'],
1853+
// `contacts.just` exists but has no `name` key, so null is injected
1854+
// and the error is reported on the concrete path.
1855+
['contacts.just.name' => 'The contacts.*.name field is required.'],
18561856
$this->validation->getErrors(),
18571857
);
18581858
}
@@ -1901,22 +1901,25 @@ public function testRequiredWildcardFailsForEachMissingElement(): void
19011901
);
19021902
}
19031903

1904-
public function testWildcardNonRequiredRuleSkipsMissingElements(): void
1904+
public function testWildcardNonRequiredRuleFiresForMissingElements(): void
19051905
{
1906-
// Without a required* rule, elements whose key does not exist must
1907-
// never be queued for validation - no false positives.
1906+
// A missing key is treated as null, consistent with non-wildcard behaviour.
1907+
// Use `if_exist` or `permit_empty` to explicitly skip absent keys.
19081908
$data = [
19091909
'contacts' => [
19101910
'friends' => [
19111911
['name' => 'Fred'], // passes in_list
1912-
['age' => 21], // key absent, must be skipped entirely
1912+
['age' => 21], // key absent - null injected, in_list fails
19131913
],
19141914
],
19151915
];
19161916

19171917
$this->validation->setRules(['contacts.friends.*.name' => 'in_list[Fred,Wilma]']);
1918-
$this->assertTrue($this->validation->run($data));
1919-
$this->assertSame([], $this->validation->getErrors());
1918+
$this->assertFalse($this->validation->run($data));
1919+
$this->assertSame(
1920+
['contacts.friends.1.name' => 'The contacts.friends.*.name field must be one of: Fred,Wilma.'],
1921+
$this->validation->getErrors(),
1922+
);
19201923
}
19211924

19221925
public function testWildcardIfExistRequiredSkipsMissingElements(): void
@@ -1939,13 +1942,13 @@ public function testWildcardIfExistRequiredSkipsMissingElements(): void
19391942

19401943
public function testWildcardPermitEmptySkipsMissingElements(): void
19411944
{
1942-
// `permit_empty` without any required* rule: an empty existing value
1943-
// passes and a missing element is never queued.
1945+
// `permit_empty` treats null as empty and short-circuits remaining rules,
1946+
// so both an explicitly empty value and an absent key (injected as null) pass.
19441947
$data = [
19451948
'contacts' => [
19461949
'friends' => [
19471950
['name' => ''], // exists but empty - permit_empty lets it through
1948-
['age' => 21], // key absent - not queued (no required* rule)
1951+
['age' => 21], // key absent - null injected, permit_empty lets it through
19491952
],
19501953
],
19511954
];
@@ -1957,9 +1960,8 @@ public function testWildcardPermitEmptySkipsMissingElements(): void
19571960

19581961
public function testWildcardRequiredWithFailsForMissingElementWhenConditionMet(): void
19591962
{
1960-
// `required_with` is a required* variant, so missing elements ARE queued.
1961-
// When the condition field is present the rule fires and the missing
1962-
// element generates an error.
1963+
// The missing key is injected as null. When the condition field is present
1964+
// the rule fires and the missing element generates an error.
19631965
$data = [
19641966
'has_friends' => '1',
19651967
'contacts' => [
@@ -1980,13 +1982,13 @@ public function testWildcardRequiredWithFailsForMissingElementWhenConditionMet()
19801982

19811983
public function testWildcardRequiredWithPassesForMissingElementWhenConditionNotMet(): void
19821984
{
1983-
// Same structure but the condition field is absent, so required_with
1984-
// does not apply and the missing element generates no error.
1985+
// The missing key is injected as null, but required_with passes because
1986+
// the condition field is absent, so no error is generated.
19851987
$data = [
19861988
'contacts' => [
19871989
'friends' => [
19881990
['name' => 'Fred', 'age' => 20], // passes
1989-
['age' => 21], // missing name, but condition absent - ok
1991+
['age' => 21], // missing name, condition absent - ok
19901992
],
19911993
],
19921994
];
@@ -2021,6 +2023,44 @@ public function testWildcardRequiredNoFalsePositiveForMissingIntermediateSegment
20212023
);
20222024
}
20232025

2026+
public function testWildcardFieldExistsFailsWhenSomeElementsMissingKey(): void
2027+
{
2028+
// field_exists uses dotKeyExists against the whole wildcard pattern, so
2029+
// it reports on the template field rather than individual concrete paths
2030+
// (unlike `required`, which reports per concrete path).
2031+
$data = [
2032+
'contacts' => [
2033+
'friends' => [
2034+
['name' => 'Fred', 'age' => 20],
2035+
['age' => 21], // 'name' key absent
2036+
],
2037+
],
2038+
];
2039+
2040+
$this->validation->setRules(['contacts.friends.*.name' => 'field_exists']);
2041+
$this->assertFalse($this->validation->run($data));
2042+
$this->assertSame(
2043+
['contacts.friends.*.name' => 'The contacts.friends.*.name field must exist.'],
2044+
$this->validation->getErrors(),
2045+
);
2046+
}
2047+
2048+
public function testWildcardFieldExistsPassesWhenAllElementsHaveKey(): void
2049+
{
2050+
$data = [
2051+
'contacts' => [
2052+
'friends' => [
2053+
['name' => 'Fred', 'age' => 20],
2054+
['name' => 'Wilma', 'age' => 25],
2055+
],
2056+
],
2057+
];
2058+
2059+
$this->validation->setRules(['contacts.friends.*.name' => 'field_exists']);
2060+
$this->assertTrue($this->validation->run($data));
2061+
$this->assertSame([], $this->validation->getErrors());
2062+
}
2063+
20242064
/**
20252065
* @param array<string, mixed> $data
20262066
* @param array<string, string> $rules

user_guide_src/source/libraries/validation.rst

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,11 +1043,6 @@ valid_url_strict Yes Fails if field does not contain a valid URL.
10431043
``FILTER_VALIDATE_URL``.
10441044
======================= ========== ============================================= ===================================================
10451045

1046-
.. note:: When validating nested array data with the wildcard ``*`` syntax, use ``required``,
1047-
``required_with``, or ``required_without`` to ensure that every array element is checked
1048-
for the presence of a key. Without one of these rules, elements whose key is absent are
1049-
silently skipped.
1050-
10511046
.. note:: You can also use any native PHP functions that return boolean and
10521047
permit at least one parameter, the field data to validate.
10531048

0 commit comments

Comments
 (0)