Skip to content

Commit 8b72997

Browse files
authored
Merge pull request #8123 from kenjis/feat-validation-field_exists
feat: [Validation] add `field_exists` rule
2 parents 3846204 + cfa4fa1 commit 8b72997

File tree

9 files changed

+200
-12
lines changed

9 files changed

+200
-12
lines changed

system/Common.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -750,14 +750,14 @@ function lang(string $line, array $args = [], ?string $locale = null)
750750
$language->setLocale($locale);
751751
}
752752

753-
$line = $language->getLine($line, $args);
753+
$lines = $language->getLine($line, $args);
754754

755755
if ($locale && $locale !== $activeLocale) {
756756
// Reset to active locale
757757
$language->setLocale($activeLocale);
758758
}
759759

760-
return $line;
760+
return $lines;
761761
}
762762
}
763763

system/Language/en/Validation.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
'differs' => 'The {field} field must differ from the {param} field.',
3030
'equals' => 'The {field} field must be exactly: {param}.',
3131
'exact_length' => 'The {field} field must be exactly {param} characters in length.',
32+
'field_exists' => 'The {field} field must exist.',
3233
'greater_than' => 'The {field} field must contain a number greater than {param}.',
3334
'greater_than_equal_to' => 'The {field} field must contain a number greater than or equal to {param}.',
3435
'hex' => 'The {field} field may only contain hexadecimal characters.',

system/Validation/Rules.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace CodeIgniter\Validation;
1313

14+
use CodeIgniter\Helpers\Array\ArrayHelper;
1415
use Config\Database;
1516
use InvalidArgumentException;
1617

@@ -427,4 +428,26 @@ public function required_without(
427428

428429
return true;
429430
}
431+
432+
/**
433+
* The field exists in $data.
434+
*
435+
* @param array|bool|float|int|object|string|null $value The field value.
436+
* @param string|null $param The rule's parameter.
437+
* @param array $data The data to be validated.
438+
* @param string|null $field The field name.
439+
*/
440+
public function field_exists(
441+
$value = null,
442+
?string $param = null,
443+
array $data = [],
444+
?string $error = null,
445+
?string $field = null
446+
): bool {
447+
if (strpos($field, '.') !== false) {
448+
return ArrayHelper::dotKeyExists($field, $data);
449+
}
450+
451+
return array_key_exists($field, $data);
452+
}
430453
}

system/Validation/StrictRules/Rules.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace CodeIgniter\Validation\StrictRules;
1515

16+
use CodeIgniter\Helpers\Array\ArrayHelper;
1617
use CodeIgniter\Validation\Rules as NonStrictRules;
1718
use Config\Database;
1819

@@ -403,4 +404,26 @@ public function required_without(
403404
): bool {
404405
return $this->nonStrictRules->required_without($str, $otherFields, $data, $error, $field);
405406
}
407+
408+
/**
409+
* The field exists in $data.
410+
*
411+
* @param array|bool|float|int|object|string|null $value The field value.
412+
* @param string|null $param The rule's parameter.
413+
* @param array $data The data to be validated.
414+
* @param string|null $field The field name.
415+
*/
416+
public function field_exists(
417+
$value = null,
418+
?string $param = null,
419+
array $data = [],
420+
?string $error = null,
421+
?string $field = null
422+
): bool {
423+
if (strpos($field, '.') !== false) {
424+
return ArrayHelper::dotKeyExists($field, $data);
425+
}
426+
427+
return array_key_exists($field, $data);
428+
}
406429
}

system/Validation/Validation.php

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
184184

185185
if ($values === []) {
186186
// We'll process the values right away if an empty array
187-
$this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data);
187+
$this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data, $field);
188188

189189
continue;
190190
}
@@ -196,7 +196,7 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
196196
}
197197
} else {
198198
// Process single field
199-
$this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data);
199+
$this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data, $field);
200200
}
201201
}
202202

@@ -323,10 +323,15 @@ protected function processRules(
323323
continue;
324324
}
325325

326-
$found = true;
327-
$passed = $param === false
328-
? $set->{$rule}($value, $error)
329-
: $set->{$rule}($value, $param, $data, $error, $field);
326+
$found = true;
327+
328+
if ($rule === 'field_exists') {
329+
$passed = $set->{$rule}($value, $param, $data, $error, $originalField);
330+
} else {
331+
$passed = ($param === false)
332+
? $set->{$rule}($value, $error)
333+
: $set->{$rule}($value, $param, $data, $error, $field);
334+
}
330335

331336
break;
332337
}
@@ -351,8 +356,10 @@ protected function processRules(
351356

352357
$param = ($param === false) ? '' : $param;
353358

359+
$fieldForErrors = ($rule === 'field_exists') ? $originalField : $field;
360+
354361
// @phpstan-ignore-next-line $error may be set by rule methods.
355-
$this->errors[$field] = $error ?? $this->getErrorMessage(
362+
$this->errors[$fieldForErrors] = $error ?? $this->getErrorMessage(
356363
($this->isClosure($rule) || $arrayCallable) ? (string) $i : $rule,
357364
$field,
358365
$label,

tests/system/Validation/RulesTest.php

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class RulesTest extends CIUnitTestCase
4747
protected function setUp(): void
4848
{
4949
parent::setUp();
50+
5051
$this->validation = new Validation((object) $this->config, Services::renderer());
5152
$this->validation->reset();
5253
}
@@ -63,6 +64,7 @@ public function testRequired(array $data, bool $expected): void
6364
public static function provideRequired(): iterable
6465
{
6566
yield from [
67+
[[], false],
6668
[['foo' => null], false],
6769
[['foo' => 123], true],
6870
[['foo' => null, 'bar' => 123], false],
@@ -138,6 +140,11 @@ public static function providePermitEmpty(): iterable
138140
{
139141
yield from [
140142
// If the rule is only `permit_empty`, any value will pass.
143+
[
144+
['foo' => 'permit_empty|valid_email'],
145+
[],
146+
true,
147+
],
141148
[
142149
['foo' => 'permit_empty|valid_email'],
143150
['foo' => ''],
@@ -203,8 +210,8 @@ public static function providePermitEmpty(): iterable
203210
['foo' => 'invalid'],
204211
false,
205212
],
206-
// Required has more priority
207213
[
214+
// Required has more priority
208215
['foo' => 'permit_empty|required|valid_email'],
209216
['foo' => ''],
210217
false,
@@ -224,8 +231,8 @@ public static function providePermitEmpty(): iterable
224231
['foo' => false],
225232
false,
226233
],
227-
// This tests will return true because the input data is trimmed
228234
[
235+
// This tests will return true because the input data is trimmed
229236
['foo' => 'permit_empty|required'],
230237
['foo' => '0'],
231238
true,
@@ -280,8 +287,8 @@ public static function providePermitEmpty(): iterable
280287
['foo' => '', 'bar' => 1],
281288
true,
282289
],
283-
// Testing with closure
284290
[
291+
// Testing with closure
285292
['foo' => ['permit_empty', static fn ($value) => true]],
286293
['foo' => ''],
287294
true,
@@ -845,4 +852,104 @@ public static function provideRequiredWithoutMultipleWithoutFields(): iterable
845852
],
846853
];
847854
}
855+
856+
/**
857+
* @dataProvider provideFieldExists
858+
*/
859+
public function testFieldExists(array $rules, array $data, bool $expected): void
860+
{
861+
$this->validation->setRules($rules);
862+
$this->assertSame($expected, $this->validation->run($data));
863+
}
864+
865+
public static function provideFieldExists(): iterable
866+
{
867+
// Do not use `foo`, because there is a lang file `Foo`, and
868+
// the error message may be messed up.
869+
yield from [
870+
'empty string' => [
871+
['fiz' => 'field_exists'],
872+
['fiz' => ''],
873+
true,
874+
],
875+
'null' => [
876+
['fiz' => 'field_exists'],
877+
['fiz' => null],
878+
true,
879+
],
880+
'false' => [
881+
['fiz' => 'field_exists'],
882+
['fiz' => false],
883+
true,
884+
],
885+
'empty array' => [
886+
['fiz' => 'field_exists'],
887+
['fiz' => []],
888+
true,
889+
],
890+
'empty data' => [
891+
['fiz' => 'field_exists'],
892+
[],
893+
false,
894+
],
895+
'dot array syntax: true' => [
896+
['fiz.bar' => 'field_exists'],
897+
[
898+
'fiz' => ['bar' => null],
899+
],
900+
true,
901+
],
902+
'dot array syntax: false' => [
903+
['fiz.bar' => 'field_exists'],
904+
[],
905+
false,
906+
],
907+
'dot array syntax asterisk: true' => [
908+
['fiz.*.baz' => 'field_exists'],
909+
[
910+
'fiz' => [
911+
'bar' => [
912+
'baz' => null,
913+
],
914+
],
915+
],
916+
true,
917+
],
918+
'dot array syntax asterisk: false' => [
919+
['fiz.*.baz' => 'field_exists'],
920+
[
921+
'fiz' => [
922+
'bar' => [
923+
'baz' => null,
924+
],
925+
'hoge' => [
926+
// 'baz' is missing.
927+
],
928+
],
929+
],
930+
false,
931+
],
932+
];
933+
}
934+
935+
public function testFieldExistsErrorMessage(): void
936+
{
937+
$this->validation->setRules(['fiz.*.baz' => 'field_exists']);
938+
$data = [
939+
'fiz' => [
940+
'bar' => [
941+
'baz' => null,
942+
],
943+
'hoge' => [
944+
// 'baz' is missing.
945+
],
946+
],
947+
];
948+
949+
$this->assertFalse($this->validation->run($data));
950+
$this->assertSame(
951+
['fiz.*.baz' => 'The fiz.*.baz field must exist.'],
952+
$this->validation->getErrors()
953+
);
954+
}
848955
}

tests/system/Validation/StrictRules/RulesTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ public function testPermitEmptyStrict(array $rules, array $data, bool $expected)
5353
public static function providePermitEmptyStrict(): iterable
5454
{
5555
yield from [
56+
[
57+
['foo' => 'permit_empty'],
58+
[],
59+
true,
60+
],
5661
[
5762
['foo' => 'permit_empty'],
5863
['foo' => ''],

user_guide_src/source/changelogs/v4.5.0.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@ Model
226226
Libraries
227227
=========
228228

229+
- **Validation:** Added the new rule ``field_exists`` that checks the filed
230+
exists in the data to be validated.
231+
229232
Helpers and Functions
230233
=====================
231234

@@ -246,6 +249,8 @@ Others
246249
Message Changes
247250
***************
248251

252+
- Added ``Validation.field_exists`` error message.
253+
249254
Changes
250255
*******
251256

user_guide_src/source/libraries/validation.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,21 @@ for including multiple Rulesets, and collections of rules that can be easily reu
275275
.. note:: You may never need to use this method, as both the :doc:`Controller </incoming/controllers>` and
276276
the :doc:`Model </models/model>` provide methods to make validation even easier.
277277

278+
********************
279+
How Validation Works
280+
********************
281+
282+
- The validation never changes data to be validated.
283+
- The validation checks each field in turn according to the Validation Rules you
284+
set. If any rule returns false, the check for that field ends there.
285+
- The Format Rules do not permit empty string. If you want to permit empty string,
286+
add the ``permit_empty`` rule.
287+
- If a field does not exist in the data to be validated, the value is interpreted
288+
as ``null``. If you want to check that the field exists, add the ``field_exists``
289+
rule.
290+
291+
.. note:: The ``field_exists`` rule can be used since v4.5.0.
292+
278293
************************
279294
Setting Validation Rules
280295
************************
@@ -898,6 +913,8 @@ differs Yes Fails if field does not differ from the one
898913
in the parameter.
899914
exact_length Yes Fails if field is not exactly the parameter ``exact_length[5]`` or ``exact_length[5,8,12]``
900915
value. One or more comma-separated values.
916+
field_exists Yes Fails if field does not exist. (This rule was
917+
added in v4.5.0.)
901918
greater_than Yes Fails if field is less than or equal to ``greater_than[8]``
902919
the parameter value or not numeric.
903920
greater_than_equal_to Yes Fails if field is less than the parameter ``greater_than_equal_to[5]``

0 commit comments

Comments
 (0)