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; } } 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.', diff --git a/system/Validation/Rules.php b/system/Validation/Rules.php index 0af02b802904..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; @@ -427,4 +428,26 @@ 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 { + 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 70d081975a62..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; @@ -403,4 +404,26 @@ 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 { + 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 747ac66d9bc4..5dbf4b7f5d93 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); } } @@ -323,10 +323,15 @@ protected function processRules( continue; } - $found = true; - $passed = $param === false - ? $set->{$rule}($value, $error) - : $set->{$rule}($value, $param, $data, $error, $field); + $found = true; + + 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; } @@ -351,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 ac2f34bf825a..4844d898e3a9 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(); } @@ -63,6 +64,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 +140,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 +210,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 +231,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 +287,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, @@ -845,4 +852,104 @@ 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 + { + // Do not use `foo`, because there is a lang file `Foo`, and + // the error message may be messed up. + yield from [ + 'empty string' => [ + ['fiz' => 'field_exists'], + ['fiz' => ''], + true, + ], + 'null' => [ + ['fiz' => 'field_exists'], + ['fiz' => null], + true, + ], + 'false' => [ + ['fiz' => 'field_exists'], + ['fiz' => false], + true, + ], + 'empty array' => [ + ['fiz' => 'field_exists'], + ['fiz' => []], + true, + ], + '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( + ['fiz.*.baz' => 'The fiz.*.baz field must exist.'], + $this->validation->getErrors() + ); + } } 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' => ''], diff --git a/user_guide_src/source/changelogs/v4.5.0.rst b/user_guide_src/source/changelogs/v4.5.0.rst index f2440ac5cc92..06434719b093 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 ===================== @@ -244,6 +247,8 @@ Others Message Changes *************** +- Added ``Validation.field_exists`` error message. + Changes ******* 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]``