@@ -1850,13 +1850,217 @@ 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 }
18591859
1860+ public function testRequiredWildcardFailsWhenSomeElementsMissingKey (): void
1861+ {
1862+ $ data = [
1863+ 'contacts ' => [
1864+ 'friends ' => [
1865+ ['name ' => 'Fred ' , 'age ' => 20 ],
1866+ ['age ' => 21 ],
1867+ ],
1868+ ],
1869+ ];
1870+
1871+ $ this ->validation ->setRules (['contacts.friends.*.name ' => 'required ' ]);
1872+ $ this ->assertFalse ($ this ->validation ->run ($ data ));
1873+ $ this ->assertSame (
1874+ ['contacts.friends.1.name ' => 'The contacts.friends.*.name field is required. ' ],
1875+ $ this ->validation ->getErrors (),
1876+ );
1877+ }
1878+
1879+ public function testRequiredWildcardFailsForEachMissingElement (): void
1880+ {
1881+ // One element has the key (creating a non-empty initial match set),
1882+ // the other two are missing it - each missing element gets its own error.
1883+ $ data = [
1884+ 'contacts ' => [
1885+ 'friends ' => [
1886+ ['name ' => 'Fred ' , 'age ' => 20 ],
1887+ ['age ' => 21 ],
1888+ ['age ' => 22 ],
1889+ ],
1890+ ],
1891+ ];
1892+
1893+ $ this ->validation ->setRules (['contacts.friends.*.name ' => 'required ' ]);
1894+ $ this ->assertFalse ($ this ->validation ->run ($ data ));
1895+ $ this ->assertSame (
1896+ [
1897+ 'contacts.friends.1.name ' => 'The contacts.friends.*.name field is required. ' ,
1898+ 'contacts.friends.2.name ' => 'The contacts.friends.*.name field is required. ' ,
1899+ ],
1900+ $ this ->validation ->getErrors (),
1901+ );
1902+ }
1903+
1904+ public function testWildcardNonRequiredRuleFiresForMissingElements (): void
1905+ {
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.
1908+ $ data = [
1909+ 'contacts ' => [
1910+ 'friends ' => [
1911+ ['name ' => 'Fred ' ], // passes in_list
1912+ ['age ' => 21 ], // key absent - null injected, in_list fails
1913+ ],
1914+ ],
1915+ ];
1916+
1917+ $ this ->validation ->setRules (['contacts.friends.*.name ' => 'in_list[Fred,Wilma] ' ]);
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+ );
1923+ }
1924+
1925+ public function testWildcardIfExistRequiredSkipsMissingElements (): void
1926+ {
1927+ // `if_exist` must short-circuit before `required` fires for elements
1928+ // whose key is absent from the data structure.
1929+ $ data = [
1930+ 'contacts ' => [
1931+ 'friends ' => [
1932+ ['name ' => 'Fred ' ], // exists and non-empty - passes
1933+ ['age ' => 21 ], // key absent - if_exist skips it
1934+ ],
1935+ ],
1936+ ];
1937+
1938+ $ this ->validation ->setRules (['contacts.friends.*.name ' => 'if_exist|required ' ]);
1939+ $ this ->assertTrue ($ this ->validation ->run ($ data ));
1940+ $ this ->assertSame ([], $ this ->validation ->getErrors ());
1941+ }
1942+
1943+ public function testWildcardPermitEmptySkipsMissingElements (): void
1944+ {
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.
1947+ $ data = [
1948+ 'contacts ' => [
1949+ 'friends ' => [
1950+ ['name ' => '' ], // exists but empty - permit_empty lets it through
1951+ ['age ' => 21 ], // key absent - null injected, permit_empty lets it through
1952+ ],
1953+ ],
1954+ ];
1955+
1956+ $ this ->validation ->setRules (['contacts.friends.*.name ' => 'permit_empty|min_length[2] ' ]);
1957+ $ this ->assertTrue ($ this ->validation ->run ($ data ));
1958+ $ this ->assertSame ([], $ this ->validation ->getErrors ());
1959+ }
1960+
1961+ public function testWildcardRequiredWithFailsForMissingElementWhenConditionMet (): void
1962+ {
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.
1965+ $ data = [
1966+ 'has_friends ' => '1 ' ,
1967+ 'contacts ' => [
1968+ 'friends ' => [
1969+ ['name ' => 'Fred ' , 'age ' => 20 ], // passes
1970+ ['age ' => 21 ], // missing name, condition met - error
1971+ ],
1972+ ],
1973+ ];
1974+
1975+ $ this ->validation ->setRules (['contacts.friends.*.name ' => 'required_with[has_friends] ' ]);
1976+ $ this ->assertFalse ($ this ->validation ->run ($ data ));
1977+ $ this ->assertSame (
1978+ ['contacts.friends.1.name ' => 'The contacts.friends.*.name field is required when has_friends is present. ' ],
1979+ $ this ->validation ->getErrors (),
1980+ );
1981+ }
1982+
1983+ public function testWildcardRequiredWithPassesForMissingElementWhenConditionNotMet (): void
1984+ {
1985+ // The missing key is injected as null, but required_with passes because
1986+ // the condition field is absent, so no error is generated.
1987+ $ data = [
1988+ 'contacts ' => [
1989+ 'friends ' => [
1990+ ['name ' => 'Fred ' , 'age ' => 20 ], // passes
1991+ ['age ' => 21 ], // missing name, condition absent - ok
1992+ ],
1993+ ],
1994+ ];
1995+
1996+ $ this ->validation ->setRules (['contacts.friends.*.name ' => 'required_with[has_friends] ' ]);
1997+ $ this ->assertTrue ($ this ->validation ->run ($ data ));
1998+ $ this ->assertSame ([], $ this ->validation ->getErrors ());
1999+ }
2000+
2001+ public function testWildcardRequiredNoFalsePositiveForMissingIntermediateSegment (): void
2002+ {
2003+ // users.1 has no `contacts` key at all - an intermediate segment is
2004+ // absent, not the leaf. Only the leaf-absent branch (users.0.contacts.1)
2005+ // should produce an error; the entirely-missing branch must be silent.
2006+ $ data = [
2007+ 'users ' => [
2008+ [
2009+ 'contacts ' => [
2010+ ['name ' => 'Alice ' ], // leaf present
2011+ ['age ' => 20 ], // leaf absent - error
2012+ ],
2013+ ],
2014+ ['age ' => 30 ], // intermediate segment `contacts` missing - no error
2015+ ],
2016+ ];
2017+
2018+ $ this ->validation ->setRules (['users.*.contacts.*.name ' => 'required ' ]);
2019+ $ this ->assertFalse ($ this ->validation ->run ($ data ));
2020+ $ this ->assertSame (
2021+ ['users.0.contacts.1.name ' => 'The users.*.contacts.*.name field is required. ' ],
2022+ $ this ->validation ->getErrors (),
2023+ );
2024+ }
2025+
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+
18602064 /**
18612065 * @param array<string, mixed> $data
18622066 * @param array<string, string> $rules
0 commit comments