• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 12739860967

13 Jan 2025 03:03AM UTC coverage: 84.454%. Remained the same
12739860967

push

github

web-flow
chore: add more trailing commas in more places (#9395)

* Apply to parameters

* Apply to array destructuring

* Apply to match

* Apply for arguments

337 of 397 new or added lines in 117 files covered. (84.89%)

1 existing line in 1 file now uncovered.

20464 of 24231 relevant lines covered (84.45%)

189.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.16
/system/Validation/Validation.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Validation;
15

16
use Closure;
17
use CodeIgniter\Database\BaseConnection;
18
use CodeIgniter\HTTP\Exceptions\HTTPException;
19
use CodeIgniter\HTTP\IncomingRequest;
20
use CodeIgniter\HTTP\Method;
21
use CodeIgniter\HTTP\RequestInterface;
22
use CodeIgniter\Validation\Exceptions\ValidationException;
23
use CodeIgniter\View\RendererInterface;
24
use Config\Validation as ValidationConfig;
25
use InvalidArgumentException;
26
use LogicException;
27
use TypeError;
28

29
/**
30
 * Validator
31
 *
32
 * @see \CodeIgniter\Validation\ValidationTest
33
 */
34
class Validation implements ValidationInterface
35
{
36
    /**
37
     * Files to load with validation functions.
38
     *
39
     * @var array
40
     */
41
    protected $ruleSetFiles;
42

43
    /**
44
     * The loaded instances of our validation files.
45
     *
46
     * @var array
47
     */
48
    protected $ruleSetInstances = [];
49

50
    /**
51
     * Stores the actual rules that should be run against $data.
52
     *
53
     * @var array<array-key, array{label?: string, rules: list<string>}>
54
     *
55
     * [
56
     *     field1 => [
57
     *         'label' => label,
58
     *         'rules' => [
59
     *              rule1, rule2, ...
60
     *          ],
61
     *     ],
62
     * ]
63
     */
64
    protected $rules = [];
65

66
    /**
67
     * The data that should be validated,
68
     * where 'key' is the alias, with value.
69
     *
70
     * @var array
71
     */
72
    protected $data = [];
73

74
    /**
75
     * The data that was actually validated.
76
     *
77
     * @var array
78
     */
79
    protected $validated = [];
80

81
    /**
82
     * Any generated errors during validation.
83
     * 'key' is the alias, 'value' is the message.
84
     *
85
     * @var array
86
     */
87
    protected $errors = [];
88

89
    /**
90
     * Stores custom error message to use
91
     * during validation. Where 'key' is the alias.
92
     *
93
     * @var array
94
     */
95
    protected $customErrors = [];
96

97
    /**
98
     * Our configuration.
99
     *
100
     * @var ValidationConfig
101
     */
102
    protected $config;
103

104
    /**
105
     * The view renderer used to render validation messages.
106
     *
107
     * @var RendererInterface
108
     */
109
    protected $view;
110

111
    /**
112
     * Validation constructor.
113
     *
114
     * @param ValidationConfig $config
115
     */
116
    public function __construct($config, RendererInterface $view)
117
    {
118
        $this->ruleSetFiles = $config->ruleSets;
1,736✔
119

120
        $this->config = $config;
1,736✔
121

122
        $this->view = $view;
1,736✔
123

124
        $this->loadRuleSets();
1,736✔
125
    }
126

127
    /**
128
     * Runs the validation process, returning true/false determining whether
129
     * validation was successful or not.
130
     *
131
     * @param array|null                                 $data    The array of data to validate.
132
     * @param string|null                                $group   The predefined group of rules to apply.
133
     * @param array|BaseConnection|non-empty-string|null $dbGroup The database group to use.
134
     */
135
    public function run(?array $data = null, ?string $group = null, $dbGroup = null): bool
136
    {
137
        if ($data === null) {
1,600✔
138
            $data = $this->data;
15✔
139
        } else {
140
            // Store data to validate.
141
            $this->data = $data;
1,585✔
142
        }
143

144
        // `DBGroup` is a reserved name. For is_unique and is_not_unique
145
        $data['DBGroup'] = $dbGroup;
1,600✔
146

147
        $this->loadRuleGroup($group);
1,600✔
148

149
        // If no rules exist, we return false to ensure
150
        // the developer didn't forget to set the rules.
151
        if ($this->rules === []) {
1,596✔
152
            return false;
5✔
153
        }
154

155
        // Replace any placeholders (e.g. {id}) in the rules with
156
        // the value found in $data, if any.
157
        $this->rules = $this->fillPlaceholders($this->rules, $data);
1,591✔
158

159
        // Need this for searching arrays in validation.
160
        helper('array');
1,589✔
161

162
        // Run through each rule. If we have any field set for
163
        // this rule, then we need to run them through!
164
        foreach ($this->rules as $field => $setup) {
1,589✔
165
            //  An array key might be int.
166
            $field = (string) $field;
1,589✔
167

168
            $rules = $setup['rules'];
1,589✔
169

170
            if (is_string($rules)) {
1,589✔
171
                $rules = $this->splitRules($rules);
×
172
            }
173

174
            if (str_contains($field, '*')) {
1,589✔
175
                $flattenedArray = array_flatten_with_dots($data);
57✔
176

177
                $values = array_filter(
57✔
178
                    $flattenedArray,
57✔
179
                    static fn ($key): bool => preg_match(self::getRegex($field), $key) === 1,
57✔
180
                    ARRAY_FILTER_USE_KEY,
57✔
181
                );
57✔
182

183
                // if keys not found
184
                $values = $values !== [] ? $values : [$field => null];
57✔
185
            } else {
186
                $values = dot_array_search($field, $data);
1,544✔
187
            }
188

189
            if ($values === []) {
1,589✔
190
                // We'll process the values right away if an empty array
191
                $this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data, $field);
14✔
192

193
                continue;
14✔
194
            }
195

196
            if (str_contains($field, '*')) {
1,575✔
197
                // Process multiple fields
198
                foreach ($values as $dotField => $value) {
57✔
199
                    $this->processRules($dotField, $setup['label'] ?? $field, $value, $rules, $data, $field);
57✔
200
                }
201
            } else {
202
                // Process single field
203
                $this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data, $field);
1,530✔
204
            }
205
        }
206

207
        if ($this->getErrors() === []) {
1,582✔
208
            // Store data that was actually validated.
209
            $this->validated = DotArrayFilter::run(
889✔
210
                array_keys($this->rules),
889✔
211
                $this->data,
889✔
212
            );
889✔
213

214
            return true;
889✔
215
        }
216

217
        return false;
703✔
218
    }
219

220
    /**
221
     * Returns regex pattern for key with dot array syntax.
222
     */
223
    private static function getRegex(string $field): string
224
    {
225
        return '/\A'
109✔
226
            . str_replace(
109✔
227
                ['\.\*', '\*\.'],
109✔
228
                ['\.[^.]+', '[^.]+\.'],
109✔
229
                preg_quote($field, '/'),
109✔
230
            )
109✔
231
            . '\z/';
109✔
232
    }
233

234
    /**
235
     * Runs the validation process, returning true or false determining whether
236
     * validation was successful or not.
237
     *
238
     * @param array|bool|float|int|object|string|null $value   The data to validate.
239
     * @param array|string                            $rules   The validation rules.
240
     * @param list<string>                            $errors  The custom error message.
241
     * @param string|null                             $dbGroup The database group to use.
242
     */
243
    public function check($value, $rules, array $errors = [], $dbGroup = null): bool
244
    {
245
        $this->reset();
33✔
246

247
        return $this->setRule(
33✔
248
            'check',
33✔
249
            null,
33✔
250
            $rules,
33✔
251
            $errors,
33✔
252
        )->run(
33✔
253
            ['check' => $value],
33✔
254
            null,
33✔
255
            $dbGroup,
33✔
256
        );
33✔
257
    }
258

259
    /**
260
     * Returns the actual validated data.
261
     */
262
    public function getValidated(): array
263
    {
264
        return $this->validated;
22✔
265
    }
266

267
    /**
268
     * Runs all of $rules against $field, until one fails, or
269
     * all of them have been processed. If one fails, it adds
270
     * the error to $this->errors and moves on to the next,
271
     * so that we can collect all of the first errors.
272
     *
273
     * @param array|string $value
274
     * @param array        $rules
275
     * @param array        $data          The array of data to validate, with `DBGroup`.
276
     * @param string|null  $originalField The original asterisk field name like "foo.*.bar".
277
     */
278
    protected function processRules(
279
        string $field,
280
        ?string $label,
281
        $value,
282
        $rules = null,       // @TODO remove `= null`
283
        ?array $data = null, // @TODO remove `= null`
284
        ?string $originalField = null,
285
    ): bool {
286
        if ($data === null) {
1,589✔
287
            throw new InvalidArgumentException('You must supply the parameter: data.');
×
288
        }
289

290
        $rules = $this->processIfExist($field, $rules, $data);
1,589✔
291
        if ($rules === true) {
1,589✔
292
            return true;
4✔
293
        }
294

295
        $rules = $this->processPermitEmpty($value, $rules, $data);
1,585✔
296
        if ($rules === true) {
1,585✔
297
            return true;
55✔
298
        }
299

300
        foreach ($rules as $i => $rule) {
1,556✔
301
            $isCallable     = is_callable($rule);
1,553✔
302
            $stringCallable = $isCallable && is_string($rule);
1,553✔
303
            $arrayCallable  = $isCallable && is_array($rule);
1,553✔
304

305
            $passed = false;
1,553✔
306
            /** @var string|null $param */
307
            $param = null;
1,553✔
308

309
            if (! $isCallable && preg_match('/(.*?)\[(.*)\]/', $rule, $match)) {
1,553✔
310
                $rule  = $match[1];
1,038✔
311
                $param = $match[2];
1,038✔
312
            }
313

314
            // Placeholder for custom errors from the rules.
315
            $error = null;
1,553✔
316

317
            // If it's a callable, call and get out of here.
318
            if ($this->isClosure($rule)) {
1,553✔
319
                $passed = $rule($value, $data, $error, $field);
14✔
320
            } elseif ($isCallable) {
1,551✔
321
                $passed = $stringCallable ? $rule($value) : $rule($value, $data, $error, $field);
44✔
322
            } else {
323
                $found = false;
1,509✔
324

325
                // Check in our rulesets
326
                foreach ($this->ruleSetInstances as $set) {
1,509✔
327
                    if (! method_exists($set, $rule)) {
1,509✔
328
                        continue;
992✔
329
                    }
330

331
                    $found = true;
1,507✔
332

333
                    if ($rule === 'field_exists') {
1,507✔
334
                        $passed = $set->{$rule}($value, $param, $data, $error, $originalField);
20✔
335
                    } else {
336
                        $passed = ($param === null)
1,487✔
337
                            ? $set->{$rule}($value, $error)
523✔
338
                            : $set->{$rule}($value, $param, $data, $error, $field);
1,038✔
339
                    }
340

341
                    break;
1,502✔
342
                }
343

344
                // If the rule wasn't found anywhere, we
345
                // should throw an exception so the developer can find it.
346
                if (! $found) {
1,504✔
347
                    throw ValidationException::forRuleNotFound($rule);
2✔
348
                }
349
            }
350

351
            // Set the error message if we didn't survive.
352
            if ($passed === false) {
1,546✔
353
                // if the $value is an array, convert it to as string representation
354
                if (is_array($value)) {
703✔
355
                    $value = $this->isStringList($value)
17✔
356
                        ? '[' . implode(', ', $value) . ']'
10✔
357
                        : json_encode($value);
7✔
358
                } elseif (is_object($value)) {
686✔
359
                    $value = json_encode($value);
2✔
360
                }
361

362
                $fieldForErrors = ($rule === 'field_exists') ? $originalField : $field;
703✔
363

364
                // @phpstan-ignore-next-line $error may be set by rule methods.
365
                $this->errors[$fieldForErrors] = $error ?? $this->getErrorMessage(
703✔
366
                    ($this->isClosure($rule) || $arrayCallable) ? (string) $i : $rule,
703✔
367
                    $field,
703✔
368
                    $label,
703✔
369
                    $param,
703✔
370
                    (string) $value,
703✔
371
                    $originalField,
703✔
372
                );
703✔
373

374
                return false;
703✔
375
            }
376
        }
377

378
        return true;
895✔
379
    }
380

381
    /**
382
     * @param array $data The array of data to validate, with `DBGroup`.
383
     *
384
     * @return array|true The modified rules or true if we return early
385
     */
386
    private function processIfExist(string $field, array $rules, array $data)
387
    {
388
        if (in_array('if_exist', $rules, true)) {
1,589✔
389
            $flattenedData = array_flatten_with_dots($data);
24✔
390
            $ifExistField  = $field;
24✔
391

392
            if (str_contains($field, '.*')) {
24✔
393
                // We'll change the dot notation into a PCRE pattern that can be used later
394
                $ifExistField   = str_replace('\.\*', '\.(?:[^\.]+)', preg_quote($field, '/'));
×
395
                $dataIsExisting = false;
×
396
                $pattern        = sprintf('/%s/u', $ifExistField);
×
397

398
                foreach (array_keys($flattenedData) as $item) {
×
399
                    if (preg_match($pattern, $item) === 1) {
×
400
                        $dataIsExisting = true;
×
401
                        break;
×
402
                    }
403
                }
404
            } elseif (str_contains($field, '.')) {
24✔
405
                $dataIsExisting = array_key_exists($ifExistField, $flattenedData);
16✔
406
            } else {
407
                $dataIsExisting = array_key_exists($ifExistField, $data);
8✔
408
            }
409

410
            if (! $dataIsExisting) {
24✔
411
                // we return early if `if_exist` is not satisfied. we have nothing to do here.
412
                return true;
4✔
413
            }
414

415
            // Otherwise remove the if_exist rule and continue the process
416
            $rules = array_filter($rules, static fn ($rule): bool => $rule instanceof Closure || $rule !== 'if_exist');
20✔
417
        }
418

419
        return $rules;
1,585✔
420
    }
421

422
    /**
423
     * @param array|string $value
424
     * @param array        $data  The array of data to validate, with `DBGroup`.
425
     *
426
     * @return array|true The modified rules or true if we return early
427
     */
428
    private function processPermitEmpty($value, array $rules, array $data)
429
    {
430
        if (in_array('permit_empty', $rules, true)) {
1,585✔
431
            if (
432
                ! in_array('required', $rules, true)
141✔
433
                && (is_array($value) ? $value === [] : trim((string) $value) === '')
141✔
434
            ) {
435
                $passed = true;
69✔
436

437
                foreach ($rules as $rule) {
69✔
438
                    if (! $this->isClosure($rule) && preg_match('/(.*?)\[(.*)\]/', $rule, $match)) {
69✔
439
                        $rule  = $match[1];
48✔
440
                        $param = $match[2];
48✔
441

442
                        if (! in_array($rule, ['required_with', 'required_without'], true)) {
48✔
443
                            continue;
18✔
444
                        }
445

446
                        // Check in our rulesets
447
                        foreach ($this->ruleSetInstances as $set) {
30✔
448
                            if (! method_exists($set, $rule)) {
30✔
449
                                continue;
×
450
                            }
451

452
                            $passed = $passed && $set->{$rule}($value, $param, $data);
30✔
453
                            break;
30✔
454
                        }
455
                    }
456
                }
457

458
                if ($passed) {
69✔
459
                    return true;
55✔
460
                }
461
            }
462

463
            $rules = array_filter($rules, static fn ($rule): bool => $rule instanceof Closure || $rule !== 'permit_empty');
90✔
464
        }
465

466
        return $rules;
1,556✔
467
    }
468

469
    /**
470
     * @param Closure(bool|float|int|list<mixed>|object|string|null, bool|float|int|list<mixed>|object|string|null, string|null, string|null): (bool|string) $rule
471
     */
472
    private function isClosure($rule): bool
473
    {
474
        return $rule instanceof Closure;
1,582✔
475
    }
476

477
    /**
478
     * Is the array a string list `list<string>`?
479
     */
480
    private function isStringList(array $array): bool
481
    {
482
        $expectedKey = 0;
17✔
483

484
        foreach ($array as $key => $val) {
17✔
485
            // Note: also covers PHP array key conversion, e.g. '5' and 5.1 both become 5
486
            if (! is_int($key)) {
7✔
487
                return false;
1✔
488
            }
489

490
            if ($key !== $expectedKey) {
6✔
491
                return false;
×
492
            }
493
            $expectedKey++;
6✔
494

495
            if (! is_string($val)) {
6✔
496
                return false;
6✔
497
            }
498
        }
499

500
        return true;
10✔
501
    }
502

503
    /**
504
     * Takes a Request object and grabs the input data to use from its
505
     * array values.
506
     */
507
    public function withRequest(RequestInterface $request): ValidationInterface
508
    {
509
        /** @var IncomingRequest $request */
510
        if (str_contains($request->getHeaderLine('Content-Type'), 'application/json')) {
25✔
511
            $this->data = $request->getJSON(true);
8✔
512

513
            if (! is_array($this->data)) {
6✔
514
                throw HTTPException::forUnsupportedJSONFormat();
2✔
515
            }
516

517
            return $this;
4✔
518
        }
519

520
        if (in_array($request->getMethod(), [Method::PUT, Method::PATCH, Method::DELETE], true)
17✔
521
            && ! str_contains($request->getHeaderLine('Content-Type'), 'multipart/form-data')
17✔
522
        ) {
523
            $this->data = $request->getRawInput();
2✔
524
        } else {
525
            $this->data = $request->getVar() ?? [];
15✔
526
        }
527

528
        return $this;
17✔
529
    }
530

531
    /**
532
     * Sets (or adds) an individual rule and custom error messages for a single
533
     * field.
534
     *
535
     * The custom error message should be just the messages that apply to
536
     * this field, like so:
537
     *    [
538
     *        'rule1' => 'message1',
539
     *        'rule2' => 'message2',
540
     *    ]
541
     *
542
     * @param array|string $rules  The validation rules.
543
     * @param array        $errors The custom error message.
544
     *
545
     * @return $this
546
     *
547
     * @throws TypeError
548
     */
549
    public function setRule(string $field, ?string $label, $rules, array $errors = [])
550
    {
551
        if (! is_array($rules) && ! is_string($rules)) {
64✔
552
            throw new TypeError('$rules must be of type string|array');
4✔
553
        }
554

555
        $ruleSet = [
60✔
556
            $field => [
60✔
557
                'label' => $label,
60✔
558
                'rules' => $rules,
60✔
559
            ],
60✔
560
        ];
60✔
561

562
        if ($errors !== []) {
60✔
563
            $ruleSet[$field]['errors'] = $errors;
4✔
564
        }
565

566
        $this->setRules(array_merge($this->getRules(), $ruleSet), $this->customErrors);
60✔
567

568
        return $this;
60✔
569
    }
570

571
    /**
572
     * Stores the rules that should be used to validate the items.
573
     *
574
     * Rules should be an array formatted like:
575
     *    [
576
     *        'field' => 'rule1|rule2'
577
     *    ]
578
     *
579
     * The $errors array should be formatted like:
580
     *    [
581
     *        'field' => [
582
     *            'rule1' => 'message1',
583
     *            'rule2' => 'message2',
584
     *        ],
585
     *    ]
586
     *
587
     * @param array $errors An array of custom error messages
588
     */
589
    public function setRules(array $rules, array $errors = []): ValidationInterface
590
    {
591
        $this->customErrors = $errors;
1,629✔
592

593
        foreach ($rules as $field => &$rule) {
1,629✔
594
            if (is_array($rule)) {
1,624✔
595
                if (array_key_exists('errors', $rule)) {
147✔
596
                    $this->customErrors[$field] = $rule['errors'];
27✔
597
                    unset($rule['errors']);
27✔
598
                }
599

600
                // if $rule is already a rule collection, just move it to "rules"
601
                // transforming [foo => [required, foobar]] to [foo => [rules => [required, foobar]]]
602
                if (! array_key_exists('rules', $rule)) {
147✔
603
                    $rule = ['rules' => $rule];
51✔
604
                }
605
            }
606

607
            if (isset($rule['rules']) && is_string($rule['rules'])) {
1,624✔
608
                $rule['rules'] = $this->splitRules($rule['rules']);
64✔
609
            }
610

611
            if (is_string($rule)) {
1,624✔
612
                $rule = ['rules' => $this->splitRules($rule)];
1,514✔
613
            }
614
        }
615

616
        $this->rules = $rules;
1,629✔
617

618
        return $this;
1,629✔
619
    }
620

621
    /**
622
     * Returns all of the rules currently defined.
623
     */
624
    public function getRules(): array
625
    {
626
        return $this->rules;
64✔
627
    }
628

629
    /**
630
     * Checks to see if the rule for key $field has been set or not.
631
     */
632
    public function hasRule(string $field): bool
633
    {
634
        return array_key_exists($field, $this->rules);
2✔
635
    }
636

637
    /**
638
     * Get rule group.
639
     *
640
     * @param string $group Group.
641
     *
642
     * @return list<string> Rule group.
643
     *
644
     * @throws ValidationException If group not found.
645
     */
646
    public function getRuleGroup(string $group): array
647
    {
648
        if (! isset($this->config->{$group})) {
14✔
649
            throw ValidationException::forGroupNotFound($group);
4✔
650
        }
651

652
        if (! is_array($this->config->{$group})) {
10✔
653
            throw ValidationException::forGroupNotArray($group);
2✔
654
        }
655

656
        return $this->config->{$group};
8✔
657
    }
658

659
    /**
660
     * Set rule group.
661
     *
662
     * @param string $group Group.
663
     *
664
     * @return void
665
     *
666
     * @throws ValidationException If group not found.
667
     */
668
    public function setRuleGroup(string $group)
669
    {
670
        $rules = $this->getRuleGroup($group);
10✔
671
        $this->setRules($rules);
6✔
672

673
        $errorName = $group . '_errors';
6✔
674
        if (isset($this->config->{$errorName})) {
6✔
675
            $this->customErrors = $this->config->{$errorName};
4✔
676
        }
677
    }
678

679
    /**
680
     * Returns the rendered HTML of the errors as defined in $template.
681
     *
682
     * You can also use validation_list_errors() in Form helper.
683
     */
684
    public function listErrors(string $template = 'list'): string
685
    {
686
        if (! array_key_exists($template, $this->config->templates)) {
3✔
687
            throw ValidationException::forInvalidTemplate($template);
2✔
688
        }
689

690
        return $this->view
1✔
691
            ->setVar('errors', $this->getErrors())
1✔
692
            ->render($this->config->templates[$template]);
1✔
693
    }
694

695
    /**
696
     * Displays a single error in formatted HTML as defined in the $template view.
697
     *
698
     * You can also use validation_show_error() in Form helper.
699
     */
700
    public function showError(string $field, string $template = 'single'): string
701
    {
702
        if (! array_key_exists($field, $this->getErrors())) {
5✔
703
            return '';
2✔
704
        }
705

706
        if (! array_key_exists($template, $this->config->templates)) {
3✔
707
            throw ValidationException::forInvalidTemplate($template);
2✔
708
        }
709

710
        return $this->view
1✔
711
            ->setVar('error', $this->getError($field))
1✔
712
            ->render($this->config->templates[$template]);
1✔
713
    }
714

715
    /**
716
     * Loads all of the rulesets classes that have been defined in the
717
     * Config\Validation and stores them locally so we can use them.
718
     *
719
     * @return void
720
     */
721
    protected function loadRuleSets()
722
    {
723
        if ($this->ruleSetFiles === [] || $this->ruleSetFiles === null) {
1,736✔
724
            throw ValidationException::forNoRuleSets();
2✔
725
        }
726

727
        foreach ($this->ruleSetFiles as $file) {
1,736✔
728
            $this->ruleSetInstances[] = new $file();
1,736✔
729
        }
730
    }
731

732
    /**
733
     * Loads custom rule groups (if set) into the current rules.
734
     *
735
     * Rules can be pre-defined in Config\Validation and can
736
     * be any name, but must all still be an array of the
737
     * same format used with setRules(). Additionally, check
738
     * for {group}_errors for an array of custom error messages.
739
     *
740
     * @param non-empty-string|null $group
741
     *
742
     * @return array<int, array> [rules, customErrors]
743
     *
744
     * @throws ValidationException
745
     */
746
    public function loadRuleGroup(?string $group = null)
747
    {
748
        if ($group === null || $group === '') {
1,601✔
749
            return [];
1,590✔
750
        }
751

752
        if (! isset($this->config->{$group})) {
24✔
753
            throw ValidationException::forGroupNotFound($group);
2✔
754
        }
755

756
        if (! is_array($this->config->{$group})) {
22✔
757
            throw ValidationException::forGroupNotArray($group);
2✔
758
        }
759

760
        $this->setRules($this->config->{$group});
20✔
761

762
        // If {group}_errors exists in the config file,
763
        // then override our custom errors with them.
764
        $errorName = $group . '_errors';
20✔
765

766
        if (isset($this->config->{$errorName})) {
20✔
767
            $this->customErrors = $this->config->{$errorName};
17✔
768
        }
769

770
        return [$this->rules, $this->customErrors];
20✔
771
    }
772

773
    /**
774
     * Replace any placeholders within the rules with the values that
775
     * match the 'key' of any properties being set. For example, if
776
     * we had the following $data array:
777
     *
778
     * [ 'id' => 13 ]
779
     *
780
     * and the following rule:
781
     *
782
     *  'is_unique[users,email,id,{id}]'
783
     *
784
     * The value of {id} would be replaced with the actual id in the form data:
785
     *
786
     *  'is_unique[users,email,id,13]'
787
     */
788
    protected function fillPlaceholders(array $rules, array $data): array
789
    {
790
        foreach ($rules as &$rule) {
1,593✔
791
            $ruleSet = $rule['rules'];
1,593✔
792

793
            foreach ($ruleSet as &$row) {
1,593✔
794
                if (is_string($row)) {
1,593✔
795
                    $placeholderFields = $this->retrievePlaceholders($row, $data);
1,593✔
796

797
                    foreach ($placeholderFields as $field) {
1,593✔
798
                        $validator ??= service('validation', null, false);
29✔
799
                        assert($validator instanceof Validation);
800

801
                        $placeholderRules = $rules[$field]['rules'] ?? null;
29✔
802

803
                        // Check if the validation rule for the placeholder exists
804
                        if ($placeholderRules === null) {
29✔
805
                            throw new LogicException(
2✔
806
                                'No validation rules for the placeholder: "' . $field
2✔
807
                                . '". You must set the validation rules for the field.'
2✔
808
                                . ' See <https://codeigniter4.github.io/userguide/libraries/validation.html#validation-placeholders>.',
2✔
809
                            );
2✔
810
                        }
811

812
                        // Check if the rule does not have placeholders
813
                        foreach ($placeholderRules as $placeholderRule) {
27✔
814
                            if ($this->retrievePlaceholders($placeholderRule, $data) !== []) {
27✔
815
                                throw new LogicException(
×
NEW
816
                                    'The placeholder field cannot use placeholder: ' . $field,
×
817
                                );
×
818
                            }
819
                        }
820

821
                        // Validate the placeholder field
822
                        $dbGroup = $data['DBGroup'] ?? null;
27✔
823
                        if (! $validator->check($data[$field], $placeholderRules, [], $dbGroup)) {
27✔
824
                            // if fails, do nothing
825
                            continue;
×
826
                        }
827

828
                        // Replace the placeholder in the rule
829
                        $ruleSet = str_replace('{' . $field . '}', (string) $data[$field], $ruleSet);
27✔
830
                    }
831
                }
832
            }
833

834
            $rule['rules'] = $ruleSet;
1,591✔
835
        }
836

837
        return $rules;
1,591✔
838
    }
839

840
    /**
841
     * Retrieves valid placeholder fields.
842
     */
843
    private function retrievePlaceholders(string $rule, array $data): array
844
    {
845
        preg_match_all('/{(.+?)}/', $rule, $matches);
1,593✔
846

847
        return array_intersect($matches[1], array_keys($data));
1,593✔
848
    }
849

850
    /**
851
     * Checks to see if an error exists for the given field.
852
     */
853
    public function hasError(string $field): bool
854
    {
855
        return (bool) preg_grep(self::getRegex($field), array_keys($this->getErrors()));
6✔
856
    }
857

858
    /**
859
     * Returns the error(s) for a specified $field (or empty string if not
860
     * set).
861
     */
862
    public function getError(?string $field = null): string
863
    {
864
        if ($field === null && count($this->rules) === 1) {
55✔
865
            $field = array_key_first($this->rules);
4✔
866
        }
867

868
        $errors = array_filter(
55✔
869
            $this->getErrors(),
55✔
870
            static fn ($key): bool => preg_match(self::getRegex($field), $key) === 1,
55✔
871
            ARRAY_FILTER_USE_KEY,
55✔
872
        );
55✔
873

874
        return $errors === [] ? '' : implode("\n", $errors);
55✔
875
    }
876

877
    /**
878
     * Returns the array of errors that were encountered during
879
     * a run() call. The array should be in the following format:
880
     *
881
     *    [
882
     *        'field1' => 'error message',
883
     *        'field2' => 'error message',
884
     *    ]
885
     *
886
     * @return array<string, string>
887
     *
888
     * @codeCoverageIgnore
889
     */
890
    public function getErrors(): array
891
    {
892
        return $this->errors;
1,594✔
893
    }
894

895
    /**
896
     * Sets the error for a specific field. Used by custom validation methods.
897
     */
898
    public function setError(string $field, string $error): ValidationInterface
899
    {
900
        $this->errors[$field] = $error;
8✔
901

902
        return $this;
8✔
903
    }
904

905
    /**
906
     * Attempts to find the appropriate error message
907
     *
908
     * @param non-empty-string|null $label
909
     * @param string|null           $value The value that caused the validation to fail.
910
     */
911
    protected function getErrorMessage(
912
        string $rule,
913
        string $field,
914
        ?string $label = null,
915
        ?string $param = null,
916
        ?string $value = null,
917
        ?string $originalField = null,
918
    ): string {
919
        $param ??= '';
697✔
920

921
        $args = [
697✔
922
            'field' => ($label === null || $label === '') ? $field : lang($label),
697✔
923
            'param' => (! isset($this->rules[$param]['label'])) ? $param : lang($this->rules[$param]['label']),
697✔
924
            'value' => $value ?? '',
697✔
925
        ];
697✔
926

927
        // Check if custom message has been defined by user
928
        if (isset($this->customErrors[$field][$rule])) {
697✔
929
            return lang($this->customErrors[$field][$rule], $args);
61✔
930
        }
931
        if (null !== $originalField && isset($this->customErrors[$originalField][$rule])) {
638✔
932
            return lang($this->customErrors[$originalField][$rule], $args);
2✔
933
        }
934

935
        // Try to grab a localized version of the message...
936
        // lang() will return the rule name back if not found,
937
        // so there will always be a string being returned.
938
        return lang('Validation.' . $rule, $args);
636✔
939
    }
940

941
    /**
942
     * Split rules string by pipe operator.
943
     */
944
    protected function splitRules(string $rules): array
945
    {
946
        if (! str_contains($rules, '|')) {
1,598✔
947
            return [$rules];
1,391✔
948
        }
949

950
        $string = $rules;
226✔
951
        $rules  = [];
226✔
952
        $length = strlen($string);
226✔
953
        $cursor = 0;
226✔
954

955
        while ($cursor < $length) {
226✔
956
            $pos = strpos($string, '|', $cursor);
226✔
957

958
            if ($pos === false) {
226✔
959
                // we're in the last rule
960
                $pos = $length;
220✔
961
            }
962

963
            $rule = substr($string, $cursor, $pos - $cursor);
226✔
964

965
            while (
966
                (substr_count($rule, '[') - substr_count($rule, '\['))
226✔
967
                !== (substr_count($rule, ']') - substr_count($rule, '\]'))
226✔
968
            ) {
969
                // the pipe is inside the brackets causing the closing bracket to
970
                // not be included. so, we adjust the rule to include that portion.
971
                $pos  = strpos($string, '|', $cursor + strlen($rule) + 1) ?: $length;
14✔
972
                $rule = substr($string, $cursor, $pos - $cursor);
14✔
973
            }
974

975
            $rules[] = $rule;
226✔
976
            $cursor += strlen($rule) + 1; // +1 to exclude the pipe
226✔
977
        }
978

979
        return array_unique($rules);
226✔
980
    }
981

982
    /**
983
     * Resets the class to a blank slate. Should be called whenever
984
     * you need to process more than one array.
985
     */
986
    public function reset(): ValidationInterface
987
    {
988
        $this->data         = [];
1,665✔
989
        $this->validated    = [];
1,665✔
990
        $this->rules        = [];
1,665✔
991
        $this->errors       = [];
1,665✔
992
        $this->customErrors = [];
1,665✔
993

994
        return $this;
1,665✔
995
    }
996
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc