From 05fdab47957dfa3eef47b8c48bd447b2f9ddd0b0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 30 Oct 2023 09:53:43 +0900 Subject: [PATCH 01/10] test: add test cases for current behavior --- tests/system/Validation/RulesTest.php | 12 +++++++++--- tests/system/Validation/StrictRules/RulesTest.php | 5 +++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/system/Validation/RulesTest.php b/tests/system/Validation/RulesTest.php index ac2f34bf825a..3d6f220cbc66 100644 --- a/tests/system/Validation/RulesTest.php +++ b/tests/system/Validation/RulesTest.php @@ -63,6 +63,7 @@ public function testRequired(array $data, bool $expected): void public static function provideRequired(): iterable { yield from [ + [[], false], [['foo' => null], false], [['foo' => 123], true], [['foo' => null, 'bar' => 123], false], @@ -138,6 +139,11 @@ public static function providePermitEmpty(): iterable { yield from [ // If the rule is only `permit_empty`, any value will pass. + [ + ['foo' => 'permit_empty|valid_email'], + [], + true, + ], [ ['foo' => 'permit_empty|valid_email'], ['foo' => ''], @@ -203,8 +209,8 @@ public static function providePermitEmpty(): iterable ['foo' => 'invalid'], false, ], - // Required has more priority [ + // Required has more priority ['foo' => 'permit_empty|required|valid_email'], ['foo' => ''], false, @@ -224,8 +230,8 @@ public static function providePermitEmpty(): iterable ['foo' => false], false, ], - // This tests will return true because the input data is trimmed [ + // This tests will return true because the input data is trimmed ['foo' => 'permit_empty|required'], ['foo' => '0'], true, @@ -280,8 +286,8 @@ public static function providePermitEmpty(): iterable ['foo' => '', 'bar' => 1], true, ], - // Testing with closure [ + // Testing with closure ['foo' => ['permit_empty', static fn ($value) => true]], ['foo' => ''], true, diff --git a/tests/system/Validation/StrictRules/RulesTest.php b/tests/system/Validation/StrictRules/RulesTest.php index 74ed4d6cecd6..6c9bcd470176 100644 --- a/tests/system/Validation/StrictRules/RulesTest.php +++ b/tests/system/Validation/StrictRules/RulesTest.php @@ -53,6 +53,11 @@ public function testPermitEmptyStrict(array $rules, array $data, bool $expected) public static function providePermitEmptyStrict(): iterable { yield from [ + [ + ['foo' => 'permit_empty'], + [], + true, + ], [ ['foo' => 'permit_empty'], ['foo' => ''], From b98fde45062689af5b97d40d7c9db51f13ae3012 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 30 Oct 2023 10:04:59 +0900 Subject: [PATCH 02/10] feat: add `field_exists` rule --- system/Validation/Rules.php | 18 +++++++++++ system/Validation/StrictRules/Rules.php | 18 +++++++++++ system/Validation/Validation.php | 5 ++-- tests/system/Validation/RulesTest.php | 40 +++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/system/Validation/Rules.php b/system/Validation/Rules.php index 0af02b802904..1ca34b8f7cc8 100644 --- a/system/Validation/Rules.php +++ b/system/Validation/Rules.php @@ -427,4 +427,22 @@ public function required_without( return true; } + + /** + * The field exists in $data. + * + * @param array|bool|float|int|object|string|null $value The field value. + * @param string|null $param The rule's parameter. + * @param array $data The data to be validated. + * @param string|null $field The field name. + */ + public function field_exists( + $value = null, + ?string $param = null, + array $data = [], + ?string $error = null, + ?string $field = null + ): bool { + return (bool) (array_key_exists($field, $data)); + } } diff --git a/system/Validation/StrictRules/Rules.php b/system/Validation/StrictRules/Rules.php index 70d081975a62..4554931efbba 100644 --- a/system/Validation/StrictRules/Rules.php +++ b/system/Validation/StrictRules/Rules.php @@ -403,4 +403,22 @@ public function required_without( ): bool { return $this->nonStrictRules->required_without($str, $otherFields, $data, $error, $field); } + + /** + * The field exists in $data. + * + * @param array|bool|float|int|object|string|null $value The field value. + * @param string|null $param The rule's parameter. + * @param array $data The data to be validated. + * @param string|null $field The field name. + */ + public function field_exists( + $value = null, + ?string $param = null, + array $data = [], + ?string $error = null, + ?string $field = null + ): bool { + return (bool) (array_key_exists($field, $data)); + } } diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index 747ac66d9bc4..63e9d701491c 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -323,8 +323,9 @@ protected function processRules( continue; } - $found = true; - $passed = $param === false + $found = true; + + $passed = ($param === false && $rule !== 'field_exists') ? $set->{$rule}($value, $error) : $set->{$rule}($value, $param, $data, $error, $field); diff --git a/tests/system/Validation/RulesTest.php b/tests/system/Validation/RulesTest.php index 3d6f220cbc66..4aa7f29a5532 100644 --- a/tests/system/Validation/RulesTest.php +++ b/tests/system/Validation/RulesTest.php @@ -851,4 +851,44 @@ public static function provideRequiredWithoutMultipleWithoutFields(): iterable ], ]; } + + /** + * @dataProvider provideFieldExists + */ + public function testFieldExists(array $rules, array $data, bool $expected): void + { + $this->validation->setRules($rules); + $this->assertSame($expected, $this->validation->run($data)); + } + + public static function provideFieldExists(): iterable + { + yield from [ + [ + ['foo' => 'field_exists'], + ['foo' => ''], + true, + ], + [ + ['foo' => 'field_exists'], + ['foo' => null], + true, + ], + [ + ['foo' => 'field_exists'], + ['foo' => false], + true, + ], + [ + ['foo' => 'field_exists'], + ['foo' => []], + true, + ], + [ + ['foo' => 'field_exists'], + [], + false, + ], + ]; + } } From 13d9541794b8c9576084aa54d9297a640b6318f0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 30 Oct 2023 10:05:25 +0900 Subject: [PATCH 03/10] docs: add about `field_exists` --- user_guide_src/source/libraries/validation.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst index b415a4e0fea7..dc76c34723c1 100644 --- a/user_guide_src/source/libraries/validation.rst +++ b/user_guide_src/source/libraries/validation.rst @@ -271,6 +271,21 @@ for including multiple Rulesets, and collections of rules that can be easily reu .. note:: You may never need to use this method, as both the :doc:`Controller ` and the :doc:`Model ` provide methods to make validation even easier. +******************** +How Validation Works +******************** + +- The validation never changes data to be validated. +- The validation checks each field in turn according to the Validation Rules you + set. If any rule returns false, the check for that field ends there. +- The Format Rules do not permit empty string. If you want to permit empty string, + add the ``permit_empty`` rule. +- If a field does not exist in the data to be validated, the value is interpreted + as ``null``. If you want to check that the field exists, add the ``field_exists`` + rule. + +.. note:: The ``field_exists`` rule can be used since v4.5.0. + ************************ Setting Validation Rules ************************ @@ -894,6 +909,8 @@ differs Yes Fails if field does not differ from the one in the parameter. exact_length Yes Fails if field is not exactly the parameter ``exact_length[5]`` or ``exact_length[5,8,12]`` value. One or more comma-separated values. +field_exists Yes Fails if field does not exist. (This rule was + added in v4.5.0.) greater_than Yes Fails if field is less than or equal to ``greater_than[8]`` the parameter value or not numeric. greater_than_equal_to Yes Fails if field is less than the parameter ``greater_than_equal_to[5]`` From a6dc7edd97a7020e84031bfce9990f4fa47dfd85 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 30 Oct 2023 10:09:33 +0900 Subject: [PATCH 04/10] docs: add changelog --- user_guide_src/source/changelogs/v4.5.0.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.5.0.rst b/user_guide_src/source/changelogs/v4.5.0.rst index f2440ac5cc92..6794ded6c51d 100644 --- a/user_guide_src/source/changelogs/v4.5.0.rst +++ b/user_guide_src/source/changelogs/v4.5.0.rst @@ -226,6 +226,9 @@ Model Libraries ========= +- **Validation:** Added the new rule ``field_exists`` that checks the filed + exists in the data to be validated. + Helpers and Functions ===================== From 6b72d1b75bd7cf0e12663f5b7f185a44edd5cd05 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 30 Oct 2023 10:22:13 +0900 Subject: [PATCH 05/10] refactor: by rector --- system/Validation/Rules.php | 2 +- system/Validation/StrictRules/Rules.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Validation/Rules.php b/system/Validation/Rules.php index 1ca34b8f7cc8..1c55c3b515b9 100644 --- a/system/Validation/Rules.php +++ b/system/Validation/Rules.php @@ -443,6 +443,6 @@ public function field_exists( ?string $error = null, ?string $field = null ): bool { - return (bool) (array_key_exists($field, $data)); + return array_key_exists($field, $data); } } diff --git a/system/Validation/StrictRules/Rules.php b/system/Validation/StrictRules/Rules.php index 4554931efbba..78c13315e3f3 100644 --- a/system/Validation/StrictRules/Rules.php +++ b/system/Validation/StrictRules/Rules.php @@ -419,6 +419,6 @@ public function field_exists( ?string $error = null, ?string $field = null ): bool { - return (bool) (array_key_exists($field, $data)); + return array_key_exists($field, $data); } } From 5945820db96d166bcc07c2df5baec6742fe716cb Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 8 Nov 2023 16:45:17 +0900 Subject: [PATCH 06/10] refactor: rename variable --- system/Common.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Common.php b/system/Common.php index 37c661d69261..75db2ce1da9d 100644 --- a/system/Common.php +++ b/system/Common.php @@ -768,14 +768,14 @@ function lang(string $line, array $args = [], ?string $locale = null) $language->setLocale($locale); } - $line = $language->getLine($line, $args); + $lines = $language->getLine($line, $args); if ($locale && $locale !== $activeLocale) { // Reset to active locale $language->setLocale($activeLocale); } - return $line; + return $lines; } } From 3145803c91f5ab1638d502dab3c5e39847da56a9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 8 Nov 2023 16:45:35 +0900 Subject: [PATCH 07/10] lang: add Validation.field_exists --- system/Language/en/Validation.php | 1 + 1 file changed, 1 insertion(+) diff --git a/system/Language/en/Validation.php b/system/Language/en/Validation.php index c78099de38f6..2a980ba4c5a6 100644 --- a/system/Language/en/Validation.php +++ b/system/Language/en/Validation.php @@ -29,6 +29,7 @@ 'differs' => 'The {field} field must differ from the {param} field.', 'equals' => 'The {field} field must be exactly: {param}.', 'exact_length' => 'The {field} field must be exactly {param} characters in length.', + 'field_exists' => 'The {field} field must exist.', 'greater_than' => 'The {field} field must contain a number greater than {param}.', 'greater_than_equal_to' => 'The {field} field must contain a number greater than or equal to {param}.', 'hex' => 'The {field} field may only contain hexadecimal characters.', From 345b6af81c6cf1f5e17cd03dbb22ed6c923b09a6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 8 Nov 2023 16:46:17 +0900 Subject: [PATCH 08/10] feat: add dot array syntax support --- system/Validation/Rules.php | 5 ++ system/Validation/StrictRules/Rules.php | 5 ++ system/Validation/Validation.php | 14 ++-- tests/system/Validation/RulesTest.php | 90 +++++++++++++++++++++---- 4 files changed, 95 insertions(+), 19 deletions(-) diff --git a/system/Validation/Rules.php b/system/Validation/Rules.php index 1c55c3b515b9..0af5be8166f0 100644 --- a/system/Validation/Rules.php +++ b/system/Validation/Rules.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Validation; +use CodeIgniter\Helpers\Array\ArrayHelper; use Config\Database; use InvalidArgumentException; @@ -443,6 +444,10 @@ public function field_exists( ?string $error = null, ?string $field = null ): bool { + if (strpos($field, '.') !== false) { + return ArrayHelper::dotKeyExists($field, $data); + } + return array_key_exists($field, $data); } } diff --git a/system/Validation/StrictRules/Rules.php b/system/Validation/StrictRules/Rules.php index 78c13315e3f3..bf671c3bec4f 100644 --- a/system/Validation/StrictRules/Rules.php +++ b/system/Validation/StrictRules/Rules.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Validation\StrictRules; +use CodeIgniter\Helpers\Array\ArrayHelper; use CodeIgniter\Validation\Rules as NonStrictRules; use Config\Database; @@ -419,6 +420,10 @@ public function field_exists( ?string $error = null, ?string $field = null ): bool { + if (strpos($field, '.') !== false) { + return ArrayHelper::dotKeyExists($field, $data); + } + return array_key_exists($field, $data); } } diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index 63e9d701491c..b03badb9bf43 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -184,7 +184,7 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup if ($values === []) { // We'll process the values right away if an empty array - $this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data); + $this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data, $field); continue; } @@ -196,7 +196,7 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup } } else { // Process single field - $this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data); + $this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data, $field); } } @@ -325,9 +325,13 @@ protected function processRules( $found = true; - $passed = ($param === false && $rule !== 'field_exists') - ? $set->{$rule}($value, $error) - : $set->{$rule}($value, $param, $data, $error, $field); + if ($rule === 'field_exists') { + $passed = $set->{$rule}($value, $param, $data, $error, $originalField); + } else { + $passed = ($param === false) + ? $set->{$rule}($value, $error) + : $set->{$rule}($value, $param, $data, $error, $field); + } break; } diff --git a/tests/system/Validation/RulesTest.php b/tests/system/Validation/RulesTest.php index 4aa7f29a5532..844daab3f005 100644 --- a/tests/system/Validation/RulesTest.php +++ b/tests/system/Validation/RulesTest.php @@ -47,6 +47,7 @@ class RulesTest extends CIUnitTestCase protected function setUp(): void { parent::setUp(); + $this->validation = new Validation((object) $this->config, Services::renderer()); $this->validation->reset(); } @@ -863,32 +864,93 @@ public function testFieldExists(array $rules, array $data, bool $expected): void public static function provideFieldExists(): iterable { + // Do not use `foo`, because there is a lang file `Foo`, and + // the error message may be messed up. yield from [ - [ - ['foo' => 'field_exists'], - ['foo' => ''], + 'empty string' => [ + ['fiz' => 'field_exists'], + ['fiz' => ''], true, ], - [ - ['foo' => 'field_exists'], - ['foo' => null], + 'null' => [ + ['fiz' => 'field_exists'], + ['fiz' => null], true, ], - [ - ['foo' => 'field_exists'], - ['foo' => false], + 'false' => [ + ['fiz' => 'field_exists'], + ['fiz' => false], true, ], - [ - ['foo' => 'field_exists'], - ['foo' => []], + 'empty array' => [ + ['fiz' => 'field_exists'], + ['fiz' => []], true, ], - [ - ['foo' => 'field_exists'], + 'empty data' => [ + ['fiz' => 'field_exists'], [], false, ], + 'dot array syntax: true' => [ + ['fiz.bar' => 'field_exists'], + [ + 'fiz' => ['bar' => null], + ], + true, + ], + 'dot array syntax: false' => [ + ['fiz.bar' => 'field_exists'], + [], + false, + ], + 'dot array syntax asterisk: true' => [ + ['fiz.*.baz' => 'field_exists'], + [ + 'fiz' => [ + 'bar' => [ + 'baz' => null, + ], + ], + ], + true, + ], + 'dot array syntax asterisk: false' => [ + ['fiz.*.baz' => 'field_exists'], + [ + 'fiz' => [ + 'bar' => [ + 'baz' => null, + ], + 'hoge' => [ + // 'baz' is missing. + ], + ], + ], + false, + ], ]; } + + public function testFieldExistsErrorMessage(): void + { + $this->validation->setRules(['fiz.*.baz' => 'field_exists']); + $data = [ + 'fiz' => [ + 'bar' => [ + 'baz' => null, + ], + 'hoge' => [ + // 'baz' is missing. + ], + ], + ]; + + $this->assertFalse($this->validation->run($data)); + $this->assertSame( + // This errror message is not perfect. + ['fiz.bar.baz' => 'The fiz.*.baz field must exist.'], + $this->validation->getErrors() + ); + } } From 05887370e90051a9c68fcc95ddb4fc77f8435d91 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 8 Nov 2023 16:49:55 +0900 Subject: [PATCH 09/10] docs: add changelog --- user_guide_src/source/changelogs/v4.5.0.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.5.0.rst b/user_guide_src/source/changelogs/v4.5.0.rst index 6794ded6c51d..06434719b093 100644 --- a/user_guide_src/source/changelogs/v4.5.0.rst +++ b/user_guide_src/source/changelogs/v4.5.0.rst @@ -247,6 +247,8 @@ Others Message Changes *************** +- Added ``Validation.field_exists`` error message. + Changes ******* From cfa4fa1181fa2fe431a97ec46ef54527b07e8603 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 8 Nov 2023 16:56:08 +0900 Subject: [PATCH 10/10] feat: improve error key --- system/Validation/Validation.php | 4 +++- tests/system/Validation/RulesTest.php | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index b03badb9bf43..5dbf4b7f5d93 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -356,8 +356,10 @@ protected function processRules( $param = ($param === false) ? '' : $param; + $fieldForErrors = ($rule === 'field_exists') ? $originalField : $field; + // @phpstan-ignore-next-line $error may be set by rule methods. - $this->errors[$field] = $error ?? $this->getErrorMessage( + $this->errors[$fieldForErrors] = $error ?? $this->getErrorMessage( ($this->isClosure($rule) || $arrayCallable) ? (string) $i : $rule, $field, $label, diff --git a/tests/system/Validation/RulesTest.php b/tests/system/Validation/RulesTest.php index 844daab3f005..4844d898e3a9 100644 --- a/tests/system/Validation/RulesTest.php +++ b/tests/system/Validation/RulesTest.php @@ -948,8 +948,7 @@ public function testFieldExistsErrorMessage(): void $this->assertFalse($this->validation->run($data)); $this->assertSame( - // This errror message is not perfect. - ['fiz.bar.baz' => 'The fiz.*.baz field must exist.'], + ['fiz.*.baz' => 'The fiz.*.baz field must exist.'], $this->validation->getErrors() ); }