Skip to content

Commit 97210e5

Browse files
authored
Merge pull request #6951 from kenjis/feat-closure-validation-rule
feat: add closure validation rule
2 parents 8ad43f5 + 02038b9 commit 97210e5

8 files changed

Lines changed: 174 additions & 8 deletions

File tree

system/Validation/Validation.php

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

1212
namespace CodeIgniter\Validation;
1313

14+
use Closure;
1415
use CodeIgniter\HTTP\IncomingRequest;
1516
use CodeIgniter\HTTP\RequestInterface;
1617
use CodeIgniter\Validation\Exceptions\ValidationException;
@@ -195,7 +196,7 @@ public function check($value, string $rule, array $errors = []): bool
195196
*
196197
* @param array|string $value
197198
* @param array|null $rules
198-
* @param array $data The array of data to validate, with `DBGroup`.
199+
* @param array|null $data The array of data to validate, with `DBGroup`.
199200
* @param string|null $originalField The original asterisk field name like "foo.*.bar".
200201
*/
201202
protected function processRules(
@@ -277,7 +278,7 @@ protected function processRules(
277278
$rules = array_diff($rules, ['permit_empty']);
278279
}
279280

280-
foreach ($rules as $rule) {
281+
foreach ($rules as $i => $rule) {
281282
$isCallable = is_callable($rule);
282283

283284
$passed = false;
@@ -292,7 +293,9 @@ protected function processRules(
292293
$error = null;
293294

294295
// If it's a callable, call and get out of here.
295-
if ($isCallable) {
296+
if ($this->isClosure($rule)) {
297+
$passed = $rule($value, $data, $error, $field);
298+
} elseif ($isCallable) {
296299
$passed = $param === false ? $rule($value) : $rule($value, $param, $data);
297300
} else {
298301
$found = false;
@@ -333,7 +336,7 @@ protected function processRules(
333336

334337
// @phpstan-ignore-next-line $error may be set by rule methods.
335338
$this->errors[$field] = $error ?? $this->getErrorMessage(
336-
$rule,
339+
$this->isClosure($rule) ? $i : $rule,
337340
$field,
338341
$label,
339342
$param,
@@ -348,6 +351,14 @@ protected function processRules(
348351
return true;
349352
}
350353

354+
/**
355+
* @param Closure|string $rule
356+
*/
357+
private function isClosure($rule): bool
358+
{
359+
return $rule instanceof Closure;
360+
}
361+
351362
/**
352363
* Is the array a string list `list<string>`?
353364
*/

tests/system/Validation/ValidationTest.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,81 @@ public function testRunDoesTheBasics(): void
204204
$this->assertFalse($this->validation->run($data));
205205
}
206206

207+
public function testClosureRule(): void
208+
{
209+
$this->validation->setRules(
210+
[
211+
'foo' => ['required', static fn ($value) => $value === 'abc'],
212+
],
213+
[
214+
// Errors
215+
'foo' => [
216+
// Specify the array key for the closure rule.
217+
1 => 'The value is not "abc"',
218+
],
219+
],
220+
);
221+
222+
$data = ['foo' => 'xyz'];
223+
$return = $this->validation->run($data);
224+
225+
$this->assertFalse($return);
226+
$this->assertSame(
227+
['foo' => 'The value is not "abc"'],
228+
$this->validation->getErrors()
229+
);
230+
}
231+
232+
public function testClosureRuleWithParamError(): void
233+
{
234+
$this->validation->setRules([
235+
'foo' => [
236+
'required',
237+
static function ($value, $data, &$error, $field) {
238+
if ($value !== 'abc') {
239+
$error = 'The ' . $field . ' value is not "abc"';
240+
241+
return false;
242+
}
243+
244+
return true;
245+
},
246+
],
247+
]);
248+
249+
$data = ['foo' => 'xyz'];
250+
$return = $this->validation->run($data);
251+
252+
$this->assertFalse($return);
253+
$this->assertSame(
254+
['foo' => 'The foo value is not "abc"'],
255+
$this->validation->getErrors()
256+
);
257+
}
258+
259+
public function testClosureRuleWithLabel(): void
260+
{
261+
$this->validation->setRules([
262+
'secret' => [
263+
'label' => 'シークレット',
264+
'rules' => ['required', static fn ($value) => $value === 'abc'],
265+
'errors' => [
266+
// Specify the array key for the closure rule.
267+
1 => 'The {field} is invalid',
268+
],
269+
],
270+
]);
271+
272+
$data = ['secret' => 'xyz'];
273+
$return = $this->validation->run($data);
274+
275+
$this->assertFalse($return);
276+
$this->assertSame(
277+
['secret' => 'The シークレット is invalid'],
278+
$this->validation->getErrors()
279+
);
280+
}
281+
207282
/**
208283
* @see https://github.com/codeigniter4/CodeIgniter4/issues/5368
209284
*

user_guide_src/source/changelogs/v4.3.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ Others
307307
- **Routing:** Added new ``$routes->view()`` method to return the view directly. See :ref:`View Routes <view-routes>`.
308308
- **View:** View Cells are now first-class citizens and can be located in the **app/Cells** directory. See :ref:`View Cells <app-cells>`.
309309
- **View:** Added ``Controlled Cells`` that provide more structure and flexibility to your View Cells. See :ref:`View Cells <controlled-cells>` for details.
310+
- **Validation:** Added Closure validation rule. See :ref:`validation-using-closure-rule` for details.
310311
- **Config:** Now you can specify Composer packages to auto-discover manually. See :ref:`Code Modules <modules-specify-composer-packages>`.
311312
- **Debug:** Kint has been updated to 5.0.1.
312313
- **Request:** Added new ``$request->getRawInputVar()`` method to return a specified variable from raw stream. See :ref:`Retrieving Raw data <incomingrequest-retrieving-raw-data>`.

user_guide_src/source/libraries/validation.rst

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -579,15 +579,26 @@ right after the name of the field the error should belong to::
579579
Creating Custom Rules
580580
*********************
581581

582+
Using Rule Classes
583+
==================
584+
582585
Rules are stored within simple, namespaced classes. They can be stored any location you would like, as long as the
583-
autoloader can find it. These files are called RuleSets. To add a new RuleSet, edit **Config/Validation.php** and
586+
autoloader can find it. These files are called RuleSets.
587+
588+
Adding a RuleSet
589+
----------------
590+
591+
To add a new RuleSet, edit **Config/Validation.php** and
584592
add the new file to the ``$ruleSets`` array:
585593

586594
.. literalinclude:: validation/033.php
587595

588596
You can add it as either a simple string with the fully qualified class name, or using the ``::class`` suffix as
589597
shown above. The primary benefit here is that it provides some extra navigation capabilities in more advanced IDEs.
590598

599+
Creating a Rule Class
600+
---------------------
601+
591602
Within the file itself, each method is a rule and must accept a string as the first parameter, and must return
592603
a boolean true or false value signifying true if it passed the test or false if it did not:
593604

@@ -599,12 +610,15 @@ second parameter:
599610

600611
.. literalinclude:: validation/035.php
601612

613+
Using a Custom Rule
614+
-------------------
615+
602616
Your new custom rule could now be used just like any other rule:
603617

604618
.. literalinclude:: validation/036.php
605619

606620
Allowing Parameters
607-
===================
621+
-------------------
608622

609623
If your method needs to work with parameters, the function will need a minimum of three parameters: the string to validate,
610624
the parameter string, and an array with all of the data that was submitted the form. The ``$data`` array is especially handy
@@ -614,6 +628,28 @@ for rules like ``required_with`` that needs to check the value of another submit
614628

615629
Custom errors can be returned as the fourth parameter, just as described above.
616630

631+
.. _validation-using-closure-rule:
632+
633+
Using Closure Rule
634+
==================
635+
636+
.. versionadded:: 4.3.0
637+
638+
If you only need the functionality of a custom rule once throughout your application,
639+
you may use a closure instead of a rule class.
640+
641+
You need to use an array for validation rules:
642+
643+
.. literalinclude:: validation/040.php
644+
645+
You must set the error message for the closure rule.
646+
When you specify the error message, set the array key for the closure rule.
647+
In the above code, the ``required`` rule has the key ``0``, and the closure has ``1``.
648+
649+
Or you can use the following parameters:
650+
651+
.. literalinclude:: validation/041.php
652+
617653
Available Rules
618654
***************
619655

user_guide_src/source/libraries/validation/003.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
class Validation
66
{
7-
public $ruleSets = [
7+
// ...
8+
9+
public array $ruleSets = [
810
\CodeIgniter\Validation\CreditCardRules::class,
911
\CodeIgniter\Validation\FileRules::class,
1012
\CodeIgniter\Validation\FormatRules::class,

user_guide_src/source/libraries/validation/033.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99

1010
class Validation
1111
{
12-
public $ruleSets = [
12+
// ...
13+
14+
public array $ruleSets = [
1315
Rules::class,
1416
FormatRules::class,
1517
FileRules::class,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
$validation->setRules(
4+
[
5+
'foo' => [
6+
'required',
7+
static fn ($value) => (int) $value % 2 === 0,
8+
],
9+
],
10+
[
11+
// Errors
12+
'foo' => [
13+
// Specify the array key for the closure rule.
14+
1 => 'The value is not even.',
15+
],
16+
],
17+
);
18+
19+
if (! $validation->run($data)) {
20+
// handle validation errors
21+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
$validation->setRules(
4+
[
5+
'foo' => [
6+
'required',
7+
static function ($value, $data, &$error, $field) {
8+
if ((int) $value % 2 === 0) {
9+
return true;
10+
}
11+
12+
$error = 'The value is not even.';
13+
14+
return false;
15+
},
16+
],
17+
],
18+
);

0 commit comments

Comments
 (0)