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

codeigniter4 / CodeIgniter4 / 7298596047

22 Dec 2023 10:01AM UTC coverage: 85.049% (+0.004%) from 85.045%
7298596047

push

github

web-flow
Merge pull request #8359 from kenjis/docs-FileLocatorInterface

[4.5] fix: merge mistake

1 of 1 new or added line in 1 file covered. (100.0%)

147 existing lines in 11 files now uncovered.

19358 of 22761 relevant lines covered (85.05%)

193.82 hits per line

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

93.79
/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\HTTP\Exceptions\HTTPException;
18
use CodeIgniter\HTTP\IncomingRequest;
19
use CodeIgniter\HTTP\Method;
20
use CodeIgniter\HTTP\RequestInterface;
21
use CodeIgniter\Validation\Exceptions\ValidationException;
22
use CodeIgniter\View\RendererInterface;
23
use Config\Services;
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
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,710✔
119

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

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

124
        $this->loadRuleSets();
1,710✔
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 string|null $dbGroup The database group to use.
134
     *
135
     * @TODO Type ?string for $dbGroup should be removed.
136
     *      See https://github.com/codeigniter4/CodeIgniter4/issues/6723
137
     */
138
    public function run(?array $data = null, ?string $group = null, ?string $dbGroup = null): bool
139
    {
140
        if ($data === null) {
1,573✔
141
            $data = $this->data;
15✔
142
        } else {
143
            // Store data to validate.
144
            $this->data = $data;
1,558✔
145
        }
146

147
        // `DBGroup` is a reserved name. For is_unique and is_not_unique
148
        $data['DBGroup'] = $dbGroup;
1,573✔
149

150
        $this->loadRuleGroup($group);
1,573✔
151

152
        // If no rules exist, we return false to ensure
153
        // the developer didn't forget to set the rules.
154
        if (empty($this->rules)) {
1,569✔
155
            return false;
5✔
156
        }
157

158
        // Replace any placeholders (e.g. {id}) in the rules with
159
        // the value found in $data, if any.
160
        $this->rules = $this->fillPlaceholders($this->rules, $data);
1,564✔
161

162
        // Need this for searching arrays in validation.
163
        helper('array');
1,564✔
164

165
        // Run through each rule. If we have any field set for
166
        // this rule, then we need to run them through!
167
        foreach ($this->rules as $field => $setup) {
1,564✔
168
            $rules = $setup['rules'];
1,564✔
169

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

174
            if (strpos($field, '*') !== false) {
1,564✔
175
                $flattenedArray = array_flatten_with_dots($data);
45✔
176

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

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

189
            if ($values === []) {
1,564✔
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 (strpos($field, '*') !== false) {
1,550✔
197
                // Process multiple fields
198
                foreach ($values as $dotField => $value) {
45✔
199
                    $this->processRules($dotField, $setup['label'] ?? $field, $value, $rules, $data, $field);
45✔
200
                }
201
            } else {
202
                // Process single field
203
                $this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data, $field);
1,509✔
204
            }
205
        }
206

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

214
            return true;
878✔
215
        }
216

217
        return false;
690✔
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'
90✔
226
            . str_replace(
90✔
227
                ['\.\*', '\*\.'],
90✔
228
                ['\.[^.]+', '[^.]+\.'],
90✔
229
                preg_quote($field, '/')
90✔
230
            )
90✔
231
            . '\z/';
90✔
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 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;
16✔
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,564✔
287
            throw new InvalidArgumentException('You must supply the parameter: data.');
×
288
        }
289

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

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

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

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

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

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

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

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

331
                    $found = true;
1,484✔
332

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

341
                    break;
1,480✔
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,482✔
347
                    throw ValidationException::forRuleNotFound($rule);
2✔
348
                }
349
            }
350

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

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

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

374
                return false;
690✔
375
            }
376
        }
377

378
        return true;
878✔
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,564✔
389
            $flattenedData = array_flatten_with_dots($data);
22✔
390
            $ifExistField  = $field;
22✔
391

392
            if (strpos($field, '.*') !== false) {
22✔
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
            } else {
405
                $dataIsExisting = array_key_exists($ifExistField, $flattenedData);
22✔
406
            }
407

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

413
            // Otherwise remove the if_exist rule and continue the process
414
            $rules = array_filter($rules, static fn ($rule) => $rule instanceof Closure || $rule !== 'if_exist');
18✔
415
        }
416

417
        return $rules;
1,560✔
418
    }
419

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

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

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

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

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

456
                if ($passed === true) {
69✔
457
                    return true;
55✔
458
                }
459
            }
460

461
            $rules = array_filter($rules, static fn ($rule) => $rule instanceof Closure || $rule !== 'permit_empty');
82✔
462
        }
463

464
        return $rules;
1,531✔
465
    }
466

467
    /**
468
     * @param Closure|string $rule
469
     */
470
    private function isClosure($rule): bool
471
    {
472
        return $rule instanceof Closure;
1,557✔
473
    }
474

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

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

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

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

498
        return true;
10✔
499
    }
500

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

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

515
            return $this;
4✔
516
        }
517

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

526
        return $this;
17✔
527
    }
528

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

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

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

564
        $this->setRules(array_merge($this->getRules(), $ruleSet), $this->customErrors);
60✔
565

566
        return $this;
60✔
567
    }
568

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

591
        foreach ($rules as $field => &$rule) {
1,602✔
592
            if (is_array($rule)) {
1,597✔
593
                if (array_key_exists('errors', $rule)) {
141✔
594
                    $this->customErrors[$field] = $rule['errors'];
24✔
595
                    unset($rule['errors']);
24✔
596
                }
597

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

605
            if (isset($rule['rules']) && is_string($rule['rules'])) {
1,597✔
606
                $rule['rules'] = $this->splitRules($rule['rules']);
61✔
607
            }
608

609
            if (is_string($rule)) {
1,597✔
610
                $rule = ['rules' => $this->splitRules($rule)];
1,493✔
611
            }
612
        }
613

614
        $this->rules = $rules;
1,602✔
615

616
        return $this;
1,602✔
617
    }
618

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

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

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

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

654
        return $this->config->{$group};
8✔
655
    }
656

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

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

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

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

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

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

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

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

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

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

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

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

758
        $this->setRules($this->config->{$group});
20✔
759

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

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

768
        return [$this->rules, $this->customErrors];
20✔
769
    }
770

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

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

795
                    foreach ($placeholderFields as $field) {
1,566✔
796
                        $validator ??= Services::validation(null, false);
27✔
797
                        assert($validator instanceof Validation);
798

799
                        $placeholderRules = $rules[$field]['rules'] ?? null;
27✔
800

801
                        // Check if the validation rule for the placeholder exists
802
                        if ($placeholderRules === null) {
27✔
803
                            throw new LogicException(
×
UNCOV
804
                                'No validation rules for the placeholder: ' . $field
×
UNCOV
805
                            );
×
806
                        }
807

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

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

824
                        // Replace the placeholder in the rule
825
                        $ruleSet = str_replace('{' . $field . '}', (string) $data[$field], $ruleSet);
27✔
826
                    }
827
                }
828
            }
829

830
            $rule['rules'] = $ruleSet;
1,566✔
831
        }
832

833
        return $rules;
1,566✔
834
    }
835

836
    /**
837
     * Retrieves valid placeholder fields.
838
     */
839
    private function retrievePlaceholders(string $rule, array $data): array
840
    {
841
        preg_match_all('/{(.+?)}/', $rule, $matches);
1,566✔
842

843
        return array_intersect($matches[1], array_keys($data));
1,566✔
844
    }
845

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

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

864
        $errors = array_filter(
51✔
865
            $this->getErrors(),
51✔
866
            static fn ($key) => preg_match(self::getRegex($field), $key),
51✔
867
            ARRAY_FILTER_USE_KEY
51✔
868
        );
51✔
869

870
        return $errors === [] ? '' : implode("\n", $errors);
51✔
871
    }
872

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

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

898
        return $this;
8✔
899
    }
900

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

917
        // Check if custom message has been defined by user
918
        if (isset($this->customErrors[$field][$rule])) {
684✔
919
            $message = lang($this->customErrors[$field][$rule]);
57✔
920
        } elseif (null !== $originalField && isset($this->customErrors[$originalField][$rule])) {
627✔
921
            $message = lang($this->customErrors[$originalField][$rule]);
2✔
922
        } else {
923
            // Try to grab a localized version of the message...
924
            // lang() will return the rule name back if not found,
925
            // so there will always be a string being returned.
926
            $message = lang('Validation.' . $rule);
625✔
927
        }
928

929
        $message = str_replace('{field}', ($label === null || $label === '') ? $field : lang($label), $message);
684✔
930
        $message = str_replace(
684✔
931
            '{param}',
684✔
932
            empty($this->rules[$param]['label']) ? $param : lang($this->rules[$param]['label']),
684✔
933
            $message
684✔
934
        );
684✔
935

936
        return str_replace('{value}', $value ?? '', $message);
684✔
937
    }
938

939
    /**
940
     * Split rules string by pipe operator.
941
     */
942
    protected function splitRules(string $rules): array
943
    {
944
        if (strpos($rules, '|') === false) {
1,574✔
945
            return [$rules];
1,371✔
946
        }
947

948
        $string = $rules;
214✔
949
        $rules  = [];
214✔
950
        $length = strlen($string);
214✔
951
        $cursor = 0;
214✔
952

953
        while ($cursor < $length) {
214✔
954
            $pos = strpos($string, '|', $cursor);
214✔
955

956
            if ($pos === false) {
214✔
957
                // we're in the last rule
958
                $pos = $length;
208✔
959
            }
960

961
            $rule = substr($string, $cursor, $pos - $cursor);
214✔
962

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

973
            $rules[] = $rule;
214✔
974
            $cursor += strlen($rule) + 1; // +1 to exclude the pipe
214✔
975
        }
976

977
        return array_unique($rules);
214✔
978
    }
979

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

992
        return $this;
1,643✔
993
    }
994
}
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

© 2025 Coveralls, Inc