From 641477b5fa9abe1c9b005abcb86062e9192bb604 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 10 Apr 2023 14:28:12 +0900 Subject: [PATCH 1/6] test: fix incorrect test --- tests/system/Validation/ValidationTest.php | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 2c6d52337fe9..36956fc3f771 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -713,19 +713,17 @@ public function testInvalidRule(): void public function testRawInput(): void { - $rawstring = 'username=admin001&role=administrator&usepass=0'; - - $data = [ - 'username' => 'admin001', - 'role' => 'administrator', - 'usepass' => 0, - ]; - + $rawstring = 'username=admin001&role=administrator&usepass=0'; $config = new App(); $config->baseURL = 'http://example.com/'; + $request = new IncomingRequest($config, new URI(), $rawstring, new UserAgent()); + + $rules = [ + 'role' => 'required|min_length[5]', + ]; + $validated = $this->validation->withRequest($request->withMethod('patch'))->setRules($rules)->run(); - $request = new IncomingRequest($config, new URI(), $rawstring, new UserAgent()); - $this->validation->withRequest($request->withMethod('patch'))->run($data); + $this->assertTrue($validated); $this->assertSame([], $this->validation->getErrors()); } From c418c84459549ec057e94fbb27072ecedb88ce1d Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Apr 2023 09:36:30 +0900 Subject: [PATCH 2/6] test: rename variable name --- tests/system/Validation/ValidationTest.php | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 36956fc3f771..245f4c8a3546 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -259,9 +259,9 @@ public function testClosureRule(): void ); $data = ['foo' => 'xyz']; - $return = $this->validation->run($data); + $result = $this->validation->run($data); - $this->assertFalse($return); + $this->assertFalse($result); $this->assertSame( ['foo' => 'The value is not "abc"'], $this->validation->getErrors() @@ -286,9 +286,9 @@ static function ($value, $data, &$error, $field) { ]); $data = ['foo' => 'xyz']; - $return = $this->validation->run($data); + $result = $this->validation->run($data); - $this->assertFalse($return); + $this->assertFalse($result); $this->assertSame( ['foo' => 'The foo value is not "abc"'], $this->validation->getErrors() @@ -309,9 +309,9 @@ public function testClosureRuleWithLabel(): void ]); $data = ['secret' => 'xyz']; - $return = $this->validation->run($data); + $result = $this->validation->run($data); - $this->assertFalse($return); + $this->assertFalse($result); $this->assertSame( ['secret' => 'The シークレット is invalid'], $this->validation->getErrors() @@ -721,9 +721,9 @@ public function testRawInput(): void $rules = [ 'role' => 'required|min_length[5]', ]; - $validated = $this->validation->withRequest($request->withMethod('patch'))->setRules($rules)->run(); + $result = $this->validation->withRequest($request->withMethod('patch'))->setRules($rules)->run(); - $this->assertTrue($validated); + $this->assertTrue($result); $this->assertSame([], $this->validation->getErrors()); } @@ -746,12 +746,12 @@ public function testJsonInput(): void $rules = [ 'role' => 'required|min_length[5]', ]; - $validated = $this->validation + $result = $this->validation ->withRequest($request->withMethod('patch')) ->setRules($rules) ->run(); - $this->assertTrue($validated); + $this->assertTrue($result); $this->assertSame([], $this->validation->getErrors()); unset($_SERVER['CONTENT_TYPE']); @@ -782,12 +782,12 @@ public function testJsonInputObjectArray(): void $rules = [ 'p' => 'required|array_count[2]', ]; - $validated = $this->validation + $result = $this->validation ->withRequest($request->withMethod('patch')) ->setRules($rules) ->run(); - $this->assertFalse($validated); + $this->assertFalse($result); $this->assertSame(['p' => 'Validation.array_count'], $this->validation->getErrors()); unset($_SERVER['CONTENT_TYPE']); From 2992fefdbf032b41aeeac5592d1c8fe8fe1237f3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Apr 2023 09:47:24 +0900 Subject: [PATCH 3/6] test: add assertions for run() results --- tests/system/Validation/ValidationTest.php | 44 +++++++++++++++------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 245f4c8a3546..99dc287d39ca 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -437,8 +437,9 @@ public function testRunWithCustomErrors(): void ], ]; $this->validation->setRules(['foo' => 'is_numeric', 'bar' => 'is_numeric'], $messages); - $this->validation->run($data); + $result = $this->validation->run($data); + $this->assertFalse($result); $this->assertSame('Nope. Not a number.', $this->validation->getError('foo')); $this->assertSame('No. Not a number.', $this->validation->getError('bar')); } @@ -464,8 +465,9 @@ public function testSetRuleWithCustomErrors(): void ['bar' => 'is_numeric'], ['is_numeric' => 'Nope. Not a number.'] ); - $this->validation->run($data); + $result = $this->validation->run($data); + $this->assertFalse($result); $this->assertSame('Nope. Not a number.', $this->validation->getError('foo')); $this->assertSame('Nope. Not a number.', $this->validation->getError('bar')); } @@ -493,7 +495,9 @@ public function testGetErrors(): void { $data = ['foo' => 'notanumber']; $this->validation->setRules(['foo' => 'is_numeric']); - $this->validation->run($data); + $result = $this->validation->run($data); + + $this->assertFalse($result); $this->assertSame(['foo' => 'Validation.is_numeric'], $this->validation->getErrors()); } @@ -501,7 +505,9 @@ public function testGetErrorsWhenNone(): void { $data = ['foo' => 123]; $this->validation->setRules(['foo' => 'is_numeric']); - $this->validation->run($data); + $result = $this->validation->run($data); + + $this->assertTrue($result); $this->assertSame([], $this->validation->getErrors()); } @@ -515,7 +521,7 @@ public function testSetErrors(): void public function testRulesReturnErrors(): void { $this->validation->setRules(['foo' => 'customError']); - $this->validation->run(['foo' => 'bar']); + $this->assertFalse($this->validation->run(['foo' => 'bar'])); $this->assertSame(['foo' => 'My lovely error'], $this->validation->getErrors()); } @@ -564,6 +570,7 @@ public function testSetRuleGroupWithCustomErrorMessage(): void $this->validation->reset(); $this->validation->setRuleGroup('login'); $this->validation->run(['username' => 'codeigniter']); + $this->assertSame([ 'password' => 'custom password required error msg.', ], $this->validation->getErrors()); @@ -915,7 +922,10 @@ public function testTagReplacement(): void 'min_length' => 'Supplied value ({value}) for {field} must have at least {param} characters.', ]] ); - $this->validation->run($data); + $result = $this->validation->run($data); + + $this->assertFalse($result); + $errors = $this->validation->getErrors(); if (! isset($errors['Username'])) { @@ -932,8 +942,10 @@ public function testRulesForObjectField(): void 'configuration' => 'required|check_object_rule', ]); - $data = (object) ['configuration' => (object) ['first' => 1, 'second' => 2]]; - $this->validation->run((array) $data); + $data = (object) ['configuration' => (object) ['first' => 1, 'second' => 2]]; + $result = $this->validation->run((array) $data); + + $this->assertTrue($result); $this->assertSame([], $this->validation->getErrors()); $this->validation->reset(); @@ -941,9 +953,10 @@ public function testRulesForObjectField(): void 'configuration' => 'required|check_object_rule', ]); - $data = (object) ['configuration' => (object) ['first1' => 1, 'second' => 2]]; - $this->validation->run((array) $data); + $data = (object) ['configuration' => (object) ['first1' => 1, 'second' => 2]]; + $result = $this->validation->run((array) $data); + $this->assertFalse($result); $this->assertSame([ 'configuration' => 'Validation.check_object_rule', ], $this->validation->getErrors()); @@ -1166,7 +1179,6 @@ public function testTranslatedLabelWithCustomErrorMessage(): void public function testTranslatedLabelTagReplacement(): void { $data = ['Username' => 'Pizza']; - $this->validation->setRules( ['Username' => [ 'label' => 'Foo.bar', @@ -1176,8 +1188,10 @@ public function testTranslatedLabelTagReplacement(): void 'min_length' => 'Foo.bar.min_length2', ]] ); + $result = $this->validation->run($data); + + $this->assertFalse($result); - $this->validation->run($data); $errors = $this->validation->getErrors(); if (! isset($errors['Username'])) { @@ -1505,8 +1519,9 @@ public function testNestedArrayThrowsException(): void 'debit_amount' => '1500', 'beneficiaries_accounts' => [], ]; - $this->validation->run($data); + $result = $this->validation->run($data); + $this->assertFalse($result); $this->assertSame([ 'beneficiaries_accounts.*.account_number' => 'The BENEFICIARY ACCOUNT NUMBER field must be exactly 5 characters in length.', 'beneficiaries_accounts.*.credit_amount' => 'The CREDIT AMOUNT field is required.', @@ -1536,8 +1551,9 @@ public function testNestedArrayThrowsException(): void ], ], ]; - $this->validation->run($data); + $result = $this->validation->run($data); + $this->assertFalse($result); $this->assertSame([ 'beneficiaries_accounts.account_3.account_number' => 'The BENEFICIARY ACCOUNT NUMBER field must be exactly 5 characters in length.', 'beneficiaries_accounts.account_2.credit_amount' => 'The CREDIT AMOUNT field is required.', From f8a7813029e6027135660bbf88de466ed5c2eb73 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Apr 2023 11:19:31 +0900 Subject: [PATCH 4/6] feat: add DotArrayFilter for Validation::getValidated() --- system/Validation/DotArrayFilter.php | 109 +++++++++++ .../system/Validation/DotArrayFilterTest.php | 183 ++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 system/Validation/DotArrayFilter.php create mode 100644 tests/system/Validation/DotArrayFilterTest.php diff --git a/system/Validation/DotArrayFilter.php b/system/Validation/DotArrayFilter.php new file mode 100644 index 000000000000..cead4f6bb414 --- /dev/null +++ b/system/Validation/DotArrayFilter.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Validation; + +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. + * + * @return array The filtered array. + */ + public static function run(array $indexes, array $array): array + { + $result = []; + + foreach ($indexes as $index) { + // See https://regex101.com/r/44Ipql/1 + $segments = preg_split( + '/(? str_replace('\.', '.', $key), + $segments + ); + + $result = array_merge_recursive($result, self::filter($segments, $array)); + } + + return $result; + } + + /** + * Used by `run()` to recursively filter the array with wildcards. + * + * @param array $indexes The dot array syntax pattern to use for filtering. + * @param array $array The array to filter. + * + * @return array The filtered array. + */ + private static function filter(array $indexes, array $array): array + { + // If index is empty, returns empty array. + if ($indexes === []) { + return []; + } + + // Grab the current index. + $currentIndex = array_shift($indexes); + + if (! isset($array[$currentIndex]) && $currentIndex !== '*') { + return []; + } + + // Handle Wildcard (*) + if ($currentIndex === '*') { + $answer = []; + + foreach ($array as $key => $value) { + if (! is_array($value)) { + continue; + } + + $result = self::filter($indexes, $value); + + if ($result !== []) { + $answer[$key] = $result; + } + } + + return $answer; + } + + // If this is the last index, make sure to return it now, + // and not try to recurse through things. + if (empty($indexes)) { + 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 ($result !== []) { + return [$currentIndex => $result]; + } + } + + // Otherwise, not found. + return []; + } +} diff --git a/tests/system/Validation/DotArrayFilterTest.php b/tests/system/Validation/DotArrayFilterTest.php new file mode 100644 index 000000000000..6d3b380f517a --- /dev/null +++ b/tests/system/Validation/DotArrayFilterTest.php @@ -0,0 +1,183 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Validation; + +use CodeIgniter\Test\CIUnitTestCase; + +/** + * @internal + * + * @group Others + */ +final class DotArrayFilterTest extends CIUnitTestCase +{ + public function testRunReturnEmptyArray() + { + $data = []; + + $result = DotArrayFilter::run(['foo.bar'], $data); + + $this->assertSame([], $result); + } + + public function testRunReturnEmptyArrayMissingValue() + { + $data = [ + 'foo' => [ + 'bar' => 23, + ], + ]; + + $result = DotArrayFilter::run(['foo.baz'], $data); + + $this->assertSame([], $result); + } + + public function testRunReturnEmptyArrayEmptyIndex() + { + $data = [ + 'foo' => [ + 'bar' => 23, + ], + ]; + + $result = DotArrayFilter::run([''], $data); + + $this->assertSame([], $result); + } + + public function testRunEarlyIndex() + { + $data = [ + 'foo' => [ + 'bar' => 23, + ], + ]; + + $result = DotArrayFilter::run(['foo'], $data); + + $this->assertSame($data, $result); + } + + public function testRunWildcard() + { + $data = [ + 'foo' => [ + 'bar' => [ + 'baz' => 23, + ], + ], + ]; + + $result = DotArrayFilter::run(['foo.*.baz'], $data); + + $this->assertSame($data, $result); + } + + public function testRunWildcardWithMultipleChoices() + { + $data = [ + 'foo' => [ + 'buzz' => [ + 'fizz' => 11, + ], + 'bar' => [ + 'baz' => 23, + ], + ], + ]; + + $result = DotArrayFilter::run(['foo.*.fizz', 'foo.*.baz'], $data); + + $this->assertSame($data, $result); + } + + public function testRunNestedNotFound() + { + $data = [ + 'foo' => [ + 'buzz' => [ + 'fizz' => 11, + ], + 'bar' => [ + 'baz' => 23, + ], + ], + ]; + + $result = DotArrayFilter::run(['foo.*.notthere'], $data); + + $this->assertSame([], $result); + } + + public function testRunIgnoresLastWildcard() + { + $data = [ + 'foo' => [ + 'bar' => [ + 'baz' => 23, + ], + ], + ]; + + $result = DotArrayFilter::run(['foo.bar.*'], $data); + + $this->assertSame($data, $result); + } + + public function testRunNestedArray() + { + $array = [ + 'user' => [ + 'name' => 'John', + 'age' => 30, + 'email' => 'john@example.com', + 'preferences' => [ + 'theme' => 'dark', + 'language' => 'en', + 'notifications' => [ + 'email' => true, + 'push' => false, + ], + ], + ], + 'product' => [ + 'name' => 'Acme Product', + 'description' => 'This is a great product!', + 'price' => 19.99, + ], + ]; + + $result = DotArrayFilter::run([ + 'user.name', + 'user.preferences.language', + 'user.preferences.notifications.email', + 'product.name', + ], $array); + + $expected = [ + 'user' => [ + 'name' => 'John', + 'preferences' => [ + 'language' => 'en', + 'notifications' => [ + 'email' => true, + ], + ], + ], + 'product' => [ + 'name' => 'Acme Product', + ], + ]; + $this->assertSame($expected, $result); + } +} From a8412edee905ddd4074768ef63f9dff7ac4128d5 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 10 Apr 2023 14:29:50 +0900 Subject: [PATCH 5/6] feat: add Validation::getValidated() for getting validated data --- system/Validation/Validation.php | 35 ++++++++++++++++++++-- tests/system/Validation/ValidationTest.php | 17 +++++++---- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index c22f38ae2413..b4131af95305 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -55,6 +55,13 @@ class Validation implements ValidationInterface */ protected $data = []; + /** + * The data that was actually validated. + * + * @var array + */ + protected $validated = []; + /** * Any generated errors during validation. * 'key' is the alias, 'value' is the message. @@ -109,7 +116,12 @@ public function __construct($config, RendererInterface $view) */ public function run(?array $data = null, ?string $group = null, ?string $dbGroup = null): bool { - $data ??= $this->data; + if ($data === null) { + $data = $this->data; + } else { + // Store data to validate. + $this->data = $data; + } // i.e. is_unique $data['DBGroup'] = $dbGroup; @@ -171,7 +183,17 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup } } - return $this->getErrors() === []; + if ($this->getErrors() === []) { + // Store data that was actually validated. + $this->validated = DotArrayFilter::run( + array_keys($this->rules), + $this->data + ); + + return true; + } + + return false; } /** @@ -188,6 +210,14 @@ public function check($value, string $rule, array $errors = []): bool return $this->setRule('check', null, $rule, $errors)->run(['check' => $value]); } + /** + * Returns actually validated data. + */ + public function getValidated(): array + { + return $this->validated; + } + /** * Runs all of $rules against $field, until one fails, or * all of them have been processed. If one fails, it adds @@ -827,6 +857,7 @@ protected function splitRules(string $rules): array public function reset(): ValidationInterface { $this->data = []; + $this->validated = []; $this->rules = []; $this->errors = []; $this->customErrors = []; diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 99dc287d39ca..1787377348e1 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -234,13 +234,16 @@ public function testRunReturnsFalseWithNothingToDo() { $this->validation->setRules([]); $this->assertFalse($this->validation->run([])); + $this->assertSame([], $this->validation->getValidated()); } public function testRunDoesTheBasics(): void { $data = ['foo' => 'notanumber']; $this->validation->setRules(['foo' => 'is_numeric']); + $this->assertFalse($this->validation->run($data)); + $this->assertSame([], $this->validation->getValidated()); } public function testClosureRule(): void @@ -266,6 +269,7 @@ public function testClosureRule(): void ['foo' => 'The value is not "abc"'], $this->validation->getErrors() ); + $this->assertSame([], $this->validation->getValidated()); } public function testClosureRuleWithParamError(): void @@ -293,6 +297,7 @@ static function ($value, $data, &$error, $field) { ['foo' => 'The foo value is not "abc"'], $this->validation->getErrors() ); + $this->assertSame([], $this->validation->getValidated()); } public function testClosureRuleWithLabel(): void @@ -732,23 +737,22 @@ public function testRawInput(): void $this->assertTrue($result); $this->assertSame([], $this->validation->getErrors()); + $this->assertSame(['role' => 'administrator'], $this->validation->getValidated()); } public function testJsonInput(): void { + $_SERVER['CONTENT_TYPE'] = 'application/json'; + $data = [ 'username' => 'admin001', 'role' => 'administrator', 'usepass' => 0, ]; - $json = json_encode($data); - - $_SERVER['CONTENT_TYPE'] = 'application/json'; - + $json = json_encode($data); $config = new App(); $config->baseURL = 'http://example.com/'; - - $request = new IncomingRequest($config, new URI(), $json, new UserAgent()); + $request = new IncomingRequest($config, new URI(), $json, new UserAgent()); $rules = [ 'role' => 'required|min_length[5]', @@ -760,6 +764,7 @@ public function testJsonInput(): void $this->assertTrue($result); $this->assertSame([], $this->validation->getErrors()); + $this->assertSame(['role' => 'administrator'], $this->validation->getValidated()); unset($_SERVER['CONTENT_TYPE']); } From 83a5d5c5e32e5b058890f51bdd7abe6e9f757edf Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 13:36:58 +0900 Subject: [PATCH 6/6] docs: add docs --- user_guide_src/source/changelogs/v4.4.0.rst | 3 +++ .../source/libraries/validation.rst | 13 ++++++++++++ .../source/libraries/validation/043.php | 21 +++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 user_guide_src/source/libraries/validation/043.php diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 3c4fe991d3c6..dd2fc83ef8b7 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -78,6 +78,9 @@ Model Libraries ========= +- **Validation:** Added ``Validation::getValidated()`` method that gets + the actual validated data. See :ref:`validation-getting-validated-data` for details. + Helpers and Functions ===================== diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index 823a8b899bb3..0eebba50b168 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -345,6 +345,19 @@ Validate one value against a rule: .. literalinclude:: validation/012.php +.. _validation-getting-validated-data: + +Getting Validated Data +====================== + +.. versionadded:: 4.4.0 + +The actual validated data can be retrieved with the ``getValidated()`` method. +This method returns an array of only those elements that have been validated by +the validation rules. + +.. literalinclude:: validation/043.php + Saving Sets of Validation Rules to the Config File ================================================== diff --git a/user_guide_src/source/libraries/validation/043.php b/user_guide_src/source/libraries/validation/043.php new file mode 100644 index 000000000000..7f3a0e0e0902 --- /dev/null +++ b/user_guide_src/source/libraries/validation/043.php @@ -0,0 +1,21 @@ +setRules([ + 'username' => 'required', + 'password' => 'required|min_length[10]', +]); + +$data = [ + 'username' => 'john', + 'password' => 'BPi-$Swu7U5lm$dX', + 'csrf_token' => '8b9218a55906f9dcc1dc263dce7f005a', +]; + +if ($validation->run($data)) { + $validatedData = $validation->getValidated(); + // $validatedData = [ + // 'username' => 'john', + // 'password' => 'BPi-$Swu7U5lm$dX', + // ]; +}