@@ -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
0 commit comments