diff --git a/system/Validation/DotArrayFilter.php b/system/Validation/DotArrayFilter.php index 62da95cb61c7..673fe67ed138 100644 --- a/system/Validation/DotArrayFilter.php +++ b/system/Validation/DotArrayFilter.php @@ -62,8 +62,9 @@ private static function filter(array $indexes, array $array): array // 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 !== '*') { + // If the current index doesn't exist and is not a wildcard, return an empty array. + // Use array_key_exists() so explicit null values are preserved. + if ($currentIndex !== '*' && ! array_key_exists($currentIndex, $array)) { return []; } @@ -88,9 +89,9 @@ private static function filter(array $indexes, array $array): array return $result; } - // If this is the last index, return the value + // If this is the last index, return the value as-is, including null. if ($indexes === []) { - return [$currentIndex => $array[$currentIndex] ?? []]; + return [$currentIndex => $array[$currentIndex]]; } // If the current value is an array, recursively filter it diff --git a/tests/system/Validation/DotArrayFilterTest.php b/tests/system/Validation/DotArrayFilterTest.php index e83b4fe9e566..84e4131102a0 100644 --- a/tests/system/Validation/DotArrayFilterTest.php +++ b/tests/system/Validation/DotArrayFilterTest.php @@ -197,4 +197,15 @@ public function testRunReturnOrderedIndices(): void $this->assertSame($data, $result); } + + public function testRunPreservesNullValue(): void + { + $data = [ + 'foo' => null, + ]; + + $result = DotArrayFilter::run(['foo'], $data); + + $this->assertSame(['foo' => null], $result); + } } diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 830296b75b97..7e0f4411f079 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -901,6 +901,49 @@ public function testJsonInput(): void service('superglobals')->unsetServer('CONTENT_TYPE'); } + /** + * @param array $rules + * @param array $expectedValidated + */ + #[DataProvider('provideGetValidatedWithNullValue')] + public function testGetValidatedWithNullValue( + array $rules, + bool $expectedResult, + array $expectedValidated, + ): void { + $data = [ + 'role' => null, + ]; + + $result = $this->validation->setRules($rules)->run($data); + + $this->assertSame($expectedResult, $result); + $this->assertSame($expectedValidated, $this->validation->getValidated()); + $this->assertSame($expectedResult ? [] : ['role'], array_keys($this->validation->getErrors())); + } + + /** + * @return iterable, + * 1: bool, + * 2: array + * }> + */ + public static function provideGetValidatedWithNullValue(): iterable + { + yield 'permit_empty preserves null' => [ + ['role' => 'permit_empty|string'], + true, + ['role' => null], + ]; + + yield 'string fails on null' => [ + ['role' => 'string'], + false, + [], + ]; + } + public function testJsonInputInvalid(): void { $this->expectException(HTTPException::class); diff --git a/user_guide_src/source/changelogs/v4.7.3.rst b/user_guide_src/source/changelogs/v4.7.3.rst index 50418133d15e..268f49ecd227 100644 --- a/user_guide_src/source/changelogs/v4.7.3.rst +++ b/user_guide_src/source/changelogs/v4.7.3.rst @@ -34,6 +34,7 @@ Bugs Fixed - **Autoloader:** Fixed a bug where ``Autoloader::unregister()`` (used during tests) silently failed to remove handlers from the SPL autoload stack, causing closures to accumulate permanently. - **Common:** Fixed a bug where the ``command()`` helper function did not properly clean up output buffers, which could lead to risky tests when exceptions were thrown. +- **Validation:** Fixed a bug where ``Validation::getValidated()`` dropped fields whose validated value was explicitly ``null``. See the repo's `CHANGELOG.md `_