Repository: codeigniter4/CodeIgniter4 (PHP)

Issue:
Bug: getValidated() missing data when double wildcard rule
### PHP Version

8.3

### CodeIgniter4 Version

4.5.5

### CodeIgniter4 Installation Method

Composer (using `codeigniter4/appstarter`)

### Which operating systems have you tested for this bug?

macOS, Linux

### Which server did you use?

apache

### Database

MySQL 5.7, 10.5.22-MariaDB

### What happened?

When running validation in the controller and grabbing the validated data with `$this->validator->getValidated()`, the data for double wild card rules (eg `dates.*.*`) is missing.

### Steps to Reproduce

Implement this controller (or the logic):
```php
<?php

namespace App\Controllers\Back;

use App\Controllers\BaseController;

class TestController extends BaseController
{
    public function test()
    {
        $data = [
            'id'    => 1,
            'dates' => [
                23 => [
                    45 => '3.4.24',
                ],
            ],
        ];

        $this->validateData($data, [
            'id'        => 'required',
            'dates.*.*' => 'required',
        ]);

        $validated = $this->validator->getValidated();

        d($data, $validated);
    }
}
```

### Expected Output

The validated data including in this case the date array.

### Anything else?

I think the problem is that the validation rule ends in a double wildcard, because when i change the array and rules to this it works:
```php
$data = [
    'id'    => 1,
    'dates' => [
        23 => [
            45 => [
                'date' => '3.4.24',
            ],
        ],
    ],
];

$this->validateData($data, [
    'id'             => 'required',
    'dates.*.*.date' => 'required',
]);

$validated = $this->validator->getValidated();

d($data, $validated);
```
Does CI4 not like rules ending with wildcards?


Discussion:
@sanchawebo can you test on my PR #9220?
Earliest i can test this would be next week, sorry.
Tested your PR and it worked for my project. Thank you very much.



Patch:
diff --git a/system/Validation/DotArrayFilter.php b/system/Validation/DotArrayFilter.php
index 4d2e1cb19ed2..c7a401e1589a 100644
--- a/system/Validation/DotArrayFilter.php
+++ b/system/Validation/DotArrayFilter.php
@@ -21,8 +21,6 @@ final class DotArrayFilter
     /**
      * Creates a new array with only the elements specified in dot array syntax.
      *
-     * This code comes from the dot_array_search() function.
-     *
      * @param array $indexes The dot array syntax pattern to use for filtering.
      * @param array $array   The array to filter.
      *
@@ -33,20 +31,14 @@ public static function run(array $indexes, array $array): array
         $result = [];
 
         foreach ($indexes as $index) {
-            // See https://regex101.com/r/44Ipql/1
-            $segments = preg_split(
-                '/(?<!\\\\)\./',
-                rtrim($index, '* '),
-                0,
-                PREG_SPLIT_NO_EMPTY
-            );
-
-            $segments = array_map(
-                static fn ($key) => str_replace('\.', '.', $key),
-                $segments
-            );
-
-            $result = array_replace_recursive($result, self::filter($segments, $array));
+            $segments = preg_split('/(?<!\\\\)\./', $index, -1, PREG_SPLIT_NO_EMPTY);
+            $segments = array_map(static fn ($key) => str_replace('\.', '.', $key), $segments);
+
+            $filteredArray = self::filter($segments, $array);
+
+            if ($filteredArray !== []) {
+                $result = array_replace_recursive($result, $filteredArray);
+            }
         }
 
         return $result;
@@ -62,53 +54,54 @@ public static function run(array $indexes, array $array): array
      */
     private static function filter(array $indexes, array $array): array
     {
-        // If index is empty, returns empty array.
+        // If there are no indexes left, return an empty array
         if ($indexes === []) {
             return [];
         }
 
-        // Grab the current index.
+        // Get the current index
         $currentIndex = array_shift($indexes);
 
+        // If the current index doesn't exist and is not a wildcard, return an empty array
         if (! isset($array[$currentIndex]) && $currentIndex !== '*') {
             return [];
         }
 
-        // Handle Wildcard (*)
+        // Handle the wildcard '*' at the current level
         if ($currentIndex === '*') {
-            $answer = [];
+            $result = [];
 
+            // Iterate over all keys at this level
             foreach ($array as $key => $value) {
-                if (! is_array($value)) {
-                    continue;
-                }
-
-                $result = self::filter($indexes, $value);
-
-                if ($result !== []) {
-                    $answer[$key] = $result;
+                if ($indexes === []) {
+                    // If no indexes are left, capture the entire value
+                    $result[$key] = $value;
+                } elseif (is_array($value)) {
+                    // If there are still indexes left, continue filtering recursively
+                    $filtered = self::filter($indexes, $value);
+                    if ($filtered !== []) {
+                        $result[$key] = $filtered;
+                    }
                 }
             }
 
-            return $answer;
+            return $result;
         }
 
-        // If this is the last index, make sure to return it now,
-        // and not try to recurse through things.
+        // If this is the last index, return the value
         if ($indexes === []) {
-            return [$currentIndex => $array[$currentIndex]];
+            return [$currentIndex => $array[$currentIndex] ?? []];
         }
 
-        // Do we need to recursively filter this value?
-        if (is_array($array[$currentIndex]) && $array[$currentIndex] !== []) {
-            $result = self::filter($indexes, $array[$currentIndex]);
+        // If the current value is an array, recursively filter it
+        if (is_array($array[$currentIndex])) {
+            $filtered = self::filter($indexes, $array[$currentIndex]);
 
-            if ($result !== []) {
-                return [$currentIndex => $result];
+            if ($filtered !== []) {
+                return [$currentIndex => $filtered];
             }
         }
 
-        // Otherwise, not found.
         return [];
     }
 }
diff --git a/user_guide_src/source/changelogs/v4.5.6.rst b/user_guide_src/source/changelogs/v4.5.6.rst
index e454409fc6a8..b982f54b38f7 100644
--- a/user_guide_src/source/changelogs/v4.5.6.rst
+++ b/user_guide_src/source/changelogs/v4.5.6.rst
@@ -31,6 +31,8 @@ Bugs Fixed
 **********
 - **Session Library:** The session initialization debug message now uses the correct log type "debug" instead of "info".
 
+- **Validation:** Fixed the `getValidated()` method that did not return valid data when validation rules used multiple asterisks.
+
 See the repo's
 `CHANGELOG.md <https://github.com/codeigniter4/CodeIgniter4/blob/develop/CHANGELOG.md>`_
 for a complete list of bugs fixed.


Test patch:
diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php
index 13991dc45916..28003dbb6477 100644
--- a/tests/system/Validation/ValidationTest.php
+++ b/tests/system/Validation/ValidationTest.php
@@ -21,6 +21,7 @@
 use CodeIgniter\Validation\Exceptions\ValidationException;
 use Config\App;
 use Config\Services;
+use Generator;
 use PHPUnit\Framework\Attributes\DataProvider;
 use PHPUnit\Framework\Attributes\Group;
 use PHPUnit\Framework\ExpectationFailedException;
@@ -1826,4 +1827,84 @@ public function testRuleWithAsteriskToMultiDimensionalArray(): void
             $this->validation->getErrors()
         );
     }
+
+    /**
+     * @param array<string, mixed>  $data
+     * @param array<string, string> $rules
+     * @param array<string, mixed>  $expectedData
+     *
+     * @see https://github.com/codeigniter4/CodeIgniter4/issues/9219
+     */
+    #[DataProvider('provideMultipleAsterisk')]
+    public function testRuleWithMultipleAsterisk(
+        array $data = [],
+        array $rules = [],
+        bool $expectedCheck = false,
+        array $expectedData = []
+    ): void {
+        $this->validation->setRules($rules);
+
+        $this->assertSame($expectedCheck, $this->validation->run($data));
+        $this->assertSame($expectedData, $this->validation->getValidated());
+    }
+
+    public static function provideMultipleAsterisk(): Generator
+    {
+        yield 'success' => [
+            [
+                'dates' => [
+                    23 => [
+                        45 => 'Its Mee!',
+                    ],
+                ],
+                'foo' => [
+                    'bar' => [
+                        'data' => [
+                            'name'     => 'John Doe',
+                            'age'      => 29,
+                            'location' => 'Indonesia',
+                        ],
+                    ],
+                ],
+            ],
+            [
+                'dates.*.*' => 'required',
+                'foo.*.*.*' => 'required',
+            ],
+            true,
+            [
+                'dates' => [
+                    23 => [
+                        45 => 'Its Mee!',
+                    ],
+                ],
+                'foo' => [
+                    'bar' => [
+                        'data' => [
+                            'name'     => 'John Doe',
+                            'age'      => 29,
+                            'location' => 'Indonesia',
+                        ],
+                    ],
+                ],
+            ],
+        ];
+
+        yield 'failed' => [
+            [
+                'foo' => [
+                    'bar' => [
+                        'data' => [
+                            'name'     => '',
+                            'age'      => 29,
+                            'location' => 'Indonesia',
+                        ],
+                    ],
+                ],
+            ],
+            ['foo.*.*.*' => 'required'],
+            false,
+            [],
+        ];
+    }
 }