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

codeigniter4 / CodeIgniter4 / 7293561159

21 Dec 2023 09:55PM UTC coverage: 85.237% (+0.004%) from 85.233%
7293561159

push

github

web-flow
Merge pull request #8355 from paulbalandan/replace

Add `replace` to composer.json

18597 of 21818 relevant lines covered (85.24%)

199.84 hits per line

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

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

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

12
namespace CodeIgniter\Validation;
13

14
use Closure;
15
use CodeIgniter\HTTP\Exceptions\HTTPException;
16
use CodeIgniter\HTTP\IncomingRequest;
17
use CodeIgniter\HTTP\RequestInterface;
18
use CodeIgniter\Validation\Exceptions\ValidationException;
19
use CodeIgniter\View\RendererInterface;
20
use Config\Services;
21
use Config\Validation as ValidationConfig;
22
use InvalidArgumentException;
23
use LogicException;
24
use TypeError;
25

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

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

47
    /**
48
     * Stores the actual rules that should be run against $data.
49
     *
50
     * @var array
51
     *
52
     * [
53
     *     field1 => [
54
     *         'label' => label,
55
     *         'rules' => [
56
     *              rule1, rule2, ...
57
     *          ],
58
     *     ],
59
     * ]
60
     */
61
    protected $rules = [];
62

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

71
    /**
72
     * The data that was actually validated.
73
     *
74
     * @var array
75
     */
76
    protected $validated = [];
77

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

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

94
    /**
95
     * Our configuration.
96
     *
97
     * @var ValidationConfig
98
     */
99
    protected $config;
100

101
    /**
102
     * The view renderer used to render validation messages.
103
     *
104
     * @var RendererInterface
105
     */
106
    protected $view;
107

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

117
        $this->config = $config;
1,908✔
118

119
        $this->view = $view;
1,908✔
120

121
        $this->loadRuleSets();
1,908✔
122
    }
123

124
    /**
125
     * Runs the validation process, returning true/false determining whether
126
     * validation was successful or not.
127
     *
128
     * @param array|null  $data    The array of data to validate.
129
     * @param string|null $group   The predefined group of rules to apply.
130
     * @param string|null $dbGroup The database group to use.
131
     *
132
     * @TODO Type ?string for $dbGroup should be removed.
133
     *      See https://github.com/codeigniter4/CodeIgniter4/issues/6723
134
     */
135
    public function run(?array $data = null, ?string $group = null, ?string $dbGroup = null): bool
136
    {
137
        if ($data === null) {
1,541✔
138
            $data = $this->data;
15✔
139
        } else {
140
            // Store data to validate.
141
            $this->data = $data;
1,526✔
142
        }
143

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

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

149
        // If no rules exist, we return false to ensure
150
        // the developer didn't forget to set the rules.
151
        if (empty($this->rules)) {
1,537✔
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,532✔
158

159
        // Need this for searching arrays in validation.
160
        helper('array');
1,532✔
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,532✔
165
            $rules = $setup['rules'];
1,532✔
166

167
            if (is_string($rules)) {
1,532✔
168
                $rules = $this->splitRules($rules);
×
169
            }
170

171
            if (strpos($field, '*') !== false) {
1,532✔
172
                $flattenedArray = array_flatten_with_dots($data);
39✔
173

174
                $values = array_filter(
39✔
175
                    $flattenedArray,
39✔
176
                    static fn ($key) => preg_match(self::getRegex($field), $key),
39✔
177
                    ARRAY_FILTER_USE_KEY
39✔
178
                );
39✔
179

180
                // if keys not found
181
                $values = $values ?: [$field => null];
39✔
182
            } else {
183
                $values = dot_array_search($field, $data);
1,497✔
184
            }
185

186
            if ($values === []) {
1,532✔
187
                // We'll process the values right away if an empty array
188
                $this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data);
12✔
189

190
                continue;
12✔
191
            }
192

193
            if (strpos($field, '*') !== false) {
1,520✔
194
                // Process multiple fields
195
                foreach ($values as $dotField => $value) {
39✔
196
                    $this->processRules($dotField, $setup['label'] ?? $field, $value, $rules, $data, $field);
39✔
197
                }
198
            } else {
199
                // Process single field
200
                $this->processRules($field, $setup['label'] ?? $field, $values, $rules, $data);
1,485✔
201
            }
202
        }
203

204
        if ($this->getErrors() === []) {
1,526✔
205
            // Store data that was actually validated.
206
            $this->validated = DotArrayFilter::run(
862✔
207
                array_keys($this->rules),
862✔
208
                $this->data
862✔
209
            );
862✔
210

211
            return true;
862✔
212
        }
213

214
        return false;
673✔
215
    }
216

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

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

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

256
    /**
257
     * Returns the actual validated data.
258
     */
259
    public function getValidated(): array
260
    {
261
        return $this->validated;
12✔
262
    }
263

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

287
        $rules = $this->processIfExist($field, $rules, $data);
1,532✔
288
        if ($rules === true) {
1,532✔
289
            return true;
4✔
290
        }
291

292
        $rules = $this->processPermitEmpty($value, $rules, $data);
1,528✔
293
        if ($rules === true) {
1,528✔
294
            return true;
52✔
295
        }
296

297
        foreach ($rules as $i => $rule) {
1,502✔
298
            $isCallable = is_callable($rule);
1,499✔
299

300
            $passed = false;
1,499✔
301
            $param  = false;
1,499✔
302

303
            if (! $isCallable && preg_match('/(.*?)\[(.*)\]/', $rule, $match)) {
1,499✔
304
                $rule  = $match[1];
1,020✔
305
                $param = $match[2];
1,020✔
306
            }
307

308
            // Placeholder for custom errors from the rules.
309
            $error = null;
1,499✔
310

311
            // If it's a callable, call and get out of here.
312
            if ($this->isClosure($rule)) {
1,499✔
313
                $passed = $rule($value, $data, $error, $field);
8✔
314
            } elseif ($isCallable) {
1,497✔
315
                $passed = $param === false ? $rule($value) : $rule($value, $param, $data);
42✔
316
            } else {
317
                $found = false;
1,457✔
318

319
                // Check in our rulesets
320
                foreach ($this->ruleSetInstances as $set) {
1,457✔
321
                    if (! method_exists($set, $rule)) {
1,457✔
322
                        continue;
982✔
323
                    }
324

325
                    $found  = true;
1,455✔
326
                    $passed = $param === false
1,455✔
327
                        ? $set->{$rule}($value, $error)
498✔
328
                        : $set->{$rule}($value, $param, $data, $error, $field);
1,020✔
329

330
                    break;
1,451✔
331
                }
332

333
                // If the rule wasn't found anywhere, we
334
                // should throw an exception so the developer can find it.
335
                if (! $found) {
1,453✔
336
                    throw ValidationException::forRuleNotFound($rule);
2✔
337
                }
338
            }
339

340
            // Set the error message if we didn't survive.
341
            if ($passed === false) {
1,493✔
342
                // if the $value is an array, convert it to as string representation
343
                if (is_array($value)) {
673✔
344
                    $value = $this->isStringList($value)
16✔
345
                        ? '[' . implode(', ', $value) . ']'
10✔
346
                        : json_encode($value);
6✔
347
                } elseif (is_object($value)) {
657✔
348
                    $value = json_encode($value);
2✔
349
                }
350

351
                $param = ($param === false) ? '' : $param;
673✔
352

353
                // @phpstan-ignore-next-line $error may be set by rule methods.
354
                $this->errors[$field] = $error ?? $this->getErrorMessage(
673✔
355
                    $this->isClosure($rule) ? $i : $rule,
673✔
356
                    $field,
673✔
357
                    $label,
673✔
358
                    $param,
673✔
359
                    (string) $value,
673✔
360
                    $originalField
673✔
361
                );
673✔
362

363
                return false;
673✔
364
            }
365
        }
366

367
        return true;
865✔
368
    }
369

370
    /**
371
     * @param array $data The array of data to validate, with `DBGroup`.
372
     *
373
     * @return array|true The modified rules or true if we return early
374
     */
375
    private function processIfExist(string $field, array $rules, array $data)
376
    {
377
        if (in_array('if_exist', $rules, true)) {
1,532✔
378
            $flattenedData = array_flatten_with_dots($data);
22✔
379
            $ifExistField  = $field;
22✔
380

381
            if (strpos($field, '.*') !== false) {
22✔
382
                // We'll change the dot notation into a PCRE pattern that can be used later
383
                $ifExistField   = str_replace('\.\*', '\.(?:[^\.]+)', preg_quote($field, '/'));
×
384
                $dataIsExisting = false;
×
385
                $pattern        = sprintf('/%s/u', $ifExistField);
×
386

387
                foreach (array_keys($flattenedData) as $item) {
×
388
                    if (preg_match($pattern, $item) === 1) {
×
389
                        $dataIsExisting = true;
×
390
                        break;
×
391
                    }
392
                }
393
            } else {
394
                $dataIsExisting = array_key_exists($ifExistField, $flattenedData);
22✔
395
            }
396

397
            if (! $dataIsExisting) {
22✔
398
                // we return early if `if_exist` is not satisfied. we have nothing to do here.
399
                return true;
4✔
400
            }
401

402
            // Otherwise remove the if_exist rule and continue the process
403
            $rules = array_filter($rules, static fn ($rule) => $rule instanceof Closure || $rule !== 'if_exist');
18✔
404
        }
405

406
        return $rules;
1,528✔
407
    }
408

409
    /**
410
     * @param array|string $value
411
     * @param array        $data  The array of data to validate, with `DBGroup`.
412
     *
413
     * @return array|true The modified rules or true if we return early
414
     */
415
    private function processPermitEmpty($value, array $rules, array $data)
416
    {
417
        if (in_array('permit_empty', $rules, true)) {
1,528✔
418
            if (
419
                ! in_array('required', $rules, true)
130✔
420
                && (is_array($value) ? $value === [] : trim((string) $value) === '')
130✔
421
            ) {
422
                $passed = true;
66✔
423

424
                foreach ($rules as $rule) {
66✔
425
                    if (! $this->isClosure($rule) && preg_match('/(.*?)\[(.*)\]/', $rule, $match)) {
66✔
426
                        $rule  = $match[1];
48✔
427
                        $param = $match[2];
48✔
428

429
                        if (! in_array($rule, ['required_with', 'required_without'], true)) {
48✔
430
                            continue;
18✔
431
                        }
432

433
                        // Check in our rulesets
434
                        foreach ($this->ruleSetInstances as $set) {
30✔
435
                            if (! method_exists($set, $rule)) {
30✔
436
                                continue;
×
437
                            }
438

439
                            $passed = $passed && $set->{$rule}($value, $param, $data);
30✔
440
                            break;
30✔
441
                        }
442
                    }
443
                }
444

445
                if ($passed === true) {
66✔
446
                    return true;
52✔
447
                }
448
            }
449

450
            $rules = array_filter($rules, static fn ($rule) => $rule instanceof Closure || $rule !== 'permit_empty');
82✔
451
        }
452

453
        return $rules;
1,502✔
454
    }
455

456
    /**
457
     * @param Closure|string $rule
458
     */
459
    private function isClosure($rule): bool
460
    {
461
        return $rule instanceof Closure;
1,525✔
462
    }
463

464
    /**
465
     * Is the array a string list `list<string>`?
466
     */
467
    private function isStringList(array $array): bool
468
    {
469
        $expectedKey = 0;
16✔
470

471
        foreach ($array as $key => $val) {
16✔
472
            // Note: also covers PHP array key conversion, e.g. '5' and 5.1 both become 5
473
            if (! is_int($key)) {
6✔
474
                return false;
×
475
            }
476

477
            if ($key !== $expectedKey) {
6✔
478
                return false;
×
479
            }
480
            $expectedKey++;
6✔
481

482
            if (! is_string($val)) {
6✔
483
                return false;
6✔
484
            }
485
        }
486

487
        return true;
10✔
488
    }
489

490
    /**
491
     * Takes a Request object and grabs the input data to use from its
492
     * array values.
493
     */
494
    public function withRequest(RequestInterface $request): ValidationInterface
495
    {
496
        /** @var IncomingRequest $request */
497
        if (strpos($request->getHeaderLine('Content-Type'), 'application/json') !== false) {
25✔
498
            $this->data = $request->getJSON(true);
8✔
499

500
            if (! is_array($this->data)) {
6✔
501
                throw HTTPException::forUnsupportedJSONFormat();
2✔
502
            }
503

504
            return $this;
4✔
505
        }
506

507
        if (in_array(strtolower($request->getMethod()), ['put', 'patch', 'delete'], true)
17✔
508
            && strpos($request->getHeaderLine('Content-Type'), 'multipart/form-data') === false
17✔
509
        ) {
510
            $this->data = $request->getRawInput();
2✔
511
        } else {
512
            $this->data = $request->getVar() ?? [];
15✔
513
        }
514

515
        return $this;
17✔
516
    }
517

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

542
        $ruleSet = [
60✔
543
            $field => [
60✔
544
                'label' => $label,
60✔
545
                'rules' => $rules,
60✔
546
            ],
60✔
547
        ];
60✔
548

549
        if ($errors !== []) {
60✔
550
            $ruleSet[$field]['errors'] = $errors;
4✔
551
        }
552

553
        $this->setRules(array_merge($this->getRules(), $ruleSet), $this->customErrors);
60✔
554

555
        return $this;
60✔
556
    }
557

558
    /**
559
     * Stores the rules that should be used to validate the items.
560
     *
561
     * Rules should be an array formatted like:
562
     *    [
563
     *        'field' => 'rule1|rule2'
564
     *    ]
565
     *
566
     * The $errors array should be formatted like:
567
     *    [
568
     *        'field' => [
569
     *            'rule1' => 'message1',
570
     *            'rule2' => 'message2',
571
     *        ],
572
     *    ]
573
     *
574
     * @param array $errors An array of custom error messages
575
     */
576
    public function setRules(array $rules, array $errors = []): ValidationInterface
577
    {
578
        $this->customErrors = $errors;
1,570✔
579

580
        foreach ($rules as $field => &$rule) {
1,570✔
581
            if (is_array($rule)) {
1,565✔
582
                if (array_key_exists('errors', $rule)) {
134✔
583
                    $this->customErrors[$field] = $rule['errors'];
22✔
584
                    unset($rule['errors']);
22✔
585
                }
586

587
                // if $rule is already a rule collection, just move it to "rules"
588
                // transforming [foo => [required, foobar]] to [foo => [rules => [required, foobar]]]
589
                if (! array_key_exists('rules', $rule)) {
134✔
590
                    $rule = ['rules' => $rule];
47✔
591
                }
592
            }
593

594
            if (isset($rule['rules']) && is_string($rule['rules'])) {
1,565✔
595
                $rule['rules'] = $this->splitRules($rule['rules']);
61✔
596
            }
597

598
            if (is_string($rule)) {
1,565✔
599
                $rule = ['rules' => $this->splitRules($rule)];
1,468✔
600
            }
601
        }
602

603
        $this->rules = $rules;
1,570✔
604

605
        return $this;
1,570✔
606
    }
607

608
    /**
609
     * Returns all of the rules currently defined.
610
     */
611
    public function getRules(): array
612
    {
613
        return $this->rules;
64✔
614
    }
615

616
    /**
617
     * Checks to see if the rule for key $field has been set or not.
618
     */
619
    public function hasRule(string $field): bool
620
    {
621
        return array_key_exists($field, $this->rules);
2✔
622
    }
623

624
    /**
625
     * Get rule group.
626
     *
627
     * @param string $group Group.
628
     *
629
     * @return string[] Rule group.
630
     *
631
     * @throws ValidationException If group not found.
632
     */
633
    public function getRuleGroup(string $group): array
634
    {
635
        if (! isset($this->config->{$group})) {
14✔
636
            throw ValidationException::forGroupNotFound($group);
4✔
637
        }
638

639
        if (! is_array($this->config->{$group})) {
10✔
640
            throw ValidationException::forGroupNotArray($group);
2✔
641
        }
642

643
        return $this->config->{$group};
8✔
644
    }
645

646
    /**
647
     * Set rule group.
648
     *
649
     * @param string $group Group.
650
     *
651
     * @return void
652
     *
653
     * @throws ValidationException If group not found.
654
     */
655
    public function setRuleGroup(string $group)
656
    {
657
        $rules = $this->getRuleGroup($group);
10✔
658
        $this->setRules($rules);
6✔
659

660
        $errorName = $group . '_errors';
6✔
661
        if (isset($this->config->{$errorName})) {
6✔
662
            $this->customErrors = $this->config->{$errorName};
4✔
663
        }
664
    }
665

666
    /**
667
     * Returns the rendered HTML of the errors as defined in $template.
668
     *
669
     * You can also use validation_list_errors() in Form helper.
670
     */
671
    public function listErrors(string $template = 'list'): string
672
    {
673
        if (! array_key_exists($template, $this->config->templates)) {
3✔
674
            throw ValidationException::forInvalidTemplate($template);
2✔
675
        }
676

677
        return $this->view
1✔
678
            ->setVar('errors', $this->getErrors())
1✔
679
            ->render($this->config->templates[$template]);
1✔
680
    }
681

682
    /**
683
     * Displays a single error in formatted HTML as defined in the $template view.
684
     *
685
     * You can also use validation_show_error() in Form helper.
686
     */
687
    public function showError(string $field, string $template = 'single'): string
688
    {
689
        if (! array_key_exists($field, $this->getErrors())) {
5✔
690
            return '';
2✔
691
        }
692

693
        if (! array_key_exists($template, $this->config->templates)) {
3✔
694
            throw ValidationException::forInvalidTemplate($template);
2✔
695
        }
696

697
        return $this->view
1✔
698
            ->setVar('error', $this->getError($field))
1✔
699
            ->render($this->config->templates[$template]);
1✔
700
    }
701

702
    /**
703
     * Loads all of the rulesets classes that have been defined in the
704
     * Config\Validation and stores them locally so we can use them.
705
     *
706
     * @return void
707
     */
708
    protected function loadRuleSets()
709
    {
710
        if (empty($this->ruleSetFiles)) {
1,908✔
711
            throw ValidationException::forNoRuleSets();
2✔
712
        }
713

714
        foreach ($this->ruleSetFiles as $file) {
1,908✔
715
            $this->ruleSetInstances[] = new $file();
1,908✔
716
        }
717
    }
718

719
    /**
720
     * Loads custom rule groups (if set) into the current rules.
721
     *
722
     * Rules can be pre-defined in Config\Validation and can
723
     * be any name, but must all still be an array of the
724
     * same format used with setRules(). Additionally, check
725
     * for {group}_errors for an array of custom error messages.
726
     *
727
     * @param non-empty-string|null $group
728
     *
729
     * @return array<int, array> [rules, customErrors]
730
     *
731
     * @throws ValidationException
732
     */
733
    public function loadRuleGroup(?string $group = null)
734
    {
735
        if ($group === null || $group === '') {
1,542✔
736
            return [];
1,531✔
737
        }
738

739
        if (! isset($this->config->{$group})) {
24✔
740
            throw ValidationException::forGroupNotFound($group);
2✔
741
        }
742

743
        if (! is_array($this->config->{$group})) {
22✔
744
            throw ValidationException::forGroupNotArray($group);
2✔
745
        }
746

747
        $this->setRules($this->config->{$group});
20✔
748

749
        // If {group}_errors exists in the config file,
750
        // then override our custom errors with them.
751
        $errorName = $group . '_errors';
20✔
752

753
        if (isset($this->config->{$errorName})) {
20✔
754
            $this->customErrors = $this->config->{$errorName};
17✔
755
        }
756

757
        return [$this->rules, $this->customErrors];
20✔
758
    }
759

760
    /**
761
     * Replace any placeholders within the rules with the values that
762
     * match the 'key' of any properties being set. For example, if
763
     * we had the following $data array:
764
     *
765
     * [ 'id' => 13 ]
766
     *
767
     * and the following rule:
768
     *
769
     *  'is_unique[users,email,id,{id}]'
770
     *
771
     * The value of {id} would be replaced with the actual id in the form data:
772
     *
773
     *  'is_unique[users,email,id,13]'
774
     */
775
    protected function fillPlaceholders(array $rules, array $data): array
776
    {
777
        foreach ($rules as &$rule) {
1,534✔
778
            $ruleSet = $rule['rules'];
1,534✔
779

780
            foreach ($ruleSet as &$row) {
1,534✔
781
                if (is_string($row)) {
1,534✔
782
                    $placeholderFields = $this->retrievePlaceholders($row, $data);
1,534✔
783

784
                    foreach ($placeholderFields as $field) {
1,534✔
785
                        $validator ??= Services::validation(null, false);
27✔
786
                        assert($validator instanceof Validation);
787

788
                        $placeholderRules = $rules[$field]['rules'] ?? null;
27✔
789

790
                        // Check if the validation rule for the placeholder exists
791
                        if ($placeholderRules === null) {
27✔
792
                            throw new LogicException(
×
793
                                'No validation rules for the placeholder: ' . $field
×
794
                            );
×
795
                        }
796

797
                        // Check if the rule does not have placeholders
798
                        foreach ($placeholderRules as $placeholderRule) {
27✔
799
                            if ($this->retrievePlaceholders($placeholderRule, $data) !== []) {
27✔
800
                                throw new LogicException(
×
801
                                    'The placeholder field cannot use placeholder: ' . $field
×
802
                                );
×
803
                            }
804
                        }
805

806
                        // Validate the placeholder field
807
                        $dbGroup = $data['DBGroup'] ?? null;
27✔
808
                        if (! $validator->check($data[$field], $placeholderRules, [], $dbGroup)) {
27✔
809
                            // if fails, do nothing
810
                            continue;
×
811
                        }
812

813
                        // Replace the placeholder in the rule
814
                        $ruleSet = str_replace('{' . $field . '}', $data[$field], $ruleSet);
27✔
815
                    }
816
                }
817
            }
818

819
            $rule['rules'] = $ruleSet;
1,534✔
820
        }
821

822
        return $rules;
1,534✔
823
    }
824

825
    /**
826
     * Retrieves valid placeholder fields.
827
     */
828
    private function retrievePlaceholders(string $rule, array $data): array
829
    {
830
        preg_match_all('/{(.+?)}/', $rule, $matches);
1,534✔
831

832
        return array_intersect($matches[1], array_keys($data));
1,534✔
833
    }
834

835
    /**
836
     * Checks to see if an error exists for the given field.
837
     */
838
    public function hasError(string $field): bool
839
    {
840
        return (bool) preg_grep(self::getRegex($field), array_keys($this->getErrors()));
2✔
841
    }
842

843
    /**
844
     * Returns the error(s) for a specified $field (or empty string if not
845
     * set).
846
     */
847
    public function getError(?string $field = null): string
848
    {
849
        if ($field === null && count($this->rules) === 1) {
50✔
850
            $field = array_key_first($this->rules);
4✔
851
        }
852

853
        $errors = array_filter(
50✔
854
            $this->getErrors(),
50✔
855
            static fn ($key) => preg_match(self::getRegex($field), $key),
50✔
856
            ARRAY_FILTER_USE_KEY
50✔
857
        );
50✔
858

859
        return $errors === [] ? '' : implode("\n", $errors);
50✔
860
    }
861

862
    /**
863
     * Returns the array of errors that were encountered during
864
     * a run() call. The array should be in the following format:
865
     *
866
     *    [
867
     *        'field1' => 'error message',
868
     *        'field2' => 'error message',
869
     *    ]
870
     *
871
     * @return array<string, string>
872
     *
873
     * @codeCoverageIgnore
874
     */
875
    public function getErrors(): array
876
    {
877
        return $this->errors;
878
    }
879

880
    /**
881
     * Sets the error for a specific field. Used by custom validation methods.
882
     */
883
    public function setError(string $field, string $error): ValidationInterface
884
    {
885
        $this->errors[$field] = $error;
8✔
886

887
        return $this;
8✔
888
    }
889

890
    /**
891
     * Attempts to find the appropriate error message
892
     *
893
     * @param non-empty-string|null $label
894
     * @param string|null           $value The value that caused the validation to fail.
895
     */
896
    protected function getErrorMessage(
897
        string $rule,
898
        string $field,
899
        ?string $label = null,
900
        ?string $param = null,
901
        ?string $value = null,
902
        ?string $originalField = null
903
    ): string {
904
        $param ??= '';
669✔
905

906
        // Check if custom message has been defined by user
907
        if (isset($this->customErrors[$field][$rule])) {
669✔
908
            $message = lang($this->customErrors[$field][$rule]);
53✔
909
        } elseif (null !== $originalField && isset($this->customErrors[$originalField][$rule])) {
616✔
910
            $message = lang($this->customErrors[$originalField][$rule]);
2✔
911
        } else {
912
            // Try to grab a localized version of the message...
913
            // lang() will return the rule name back if not found,
914
            // so there will always be a string being returned.
915
            $message = lang('Validation.' . $rule);
614✔
916
        }
917

918
        $message = str_replace('{field}', ($label === null || $label === '') ? $field : lang($label), $message);
669✔
919
        $message = str_replace(
669✔
920
            '{param}',
669✔
921
            empty($this->rules[$param]['label']) ? $param : lang($this->rules[$param]['label']),
669✔
922
            $message
669✔
923
        );
669✔
924

925
        return str_replace('{value}', $value ?? '', $message);
669✔
926
    }
927

928
    /**
929
     * Split rules string by pipe operator.
930
     */
931
    protected function splitRules(string $rules): array
932
    {
933
        if (strpos($rules, '|') === false) {
1,549✔
934
            return [$rules];
1,348✔
935
        }
936

937
        $string = $rules;
212✔
938
        $rules  = [];
212✔
939
        $length = strlen($string);
212✔
940
        $cursor = 0;
212✔
941

942
        while ($cursor < $length) {
212✔
943
            $pos = strpos($string, '|', $cursor);
212✔
944

945
            if ($pos === false) {
212✔
946
                // we're in the last rule
947
                $pos = $length;
206✔
948
            }
949

950
            $rule = substr($string, $cursor, $pos - $cursor);
212✔
951

952
            while (
953
                (substr_count($rule, '[') - substr_count($rule, '\['))
212✔
954
                !== (substr_count($rule, ']') - substr_count($rule, '\]'))
212✔
955
            ) {
956
                // the pipe is inside the brackets causing the closing bracket to
957
                // not be included. so, we adjust the rule to include that portion.
958
                $pos  = strpos($string, '|', $cursor + strlen($rule) + 1) ?: $length;
14✔
959
                $rule = substr($string, $cursor, $pos - $cursor);
14✔
960
            }
961

962
            $rules[] = $rule;
212✔
963
            $cursor += strlen($rule) + 1; // +1 to exclude the pipe
212✔
964
        }
965

966
        return array_unique($rules);
212✔
967
    }
968

969
    /**
970
     * Resets the class to a blank slate. Should be called whenever
971
     * you need to process more than one array.
972
     */
973
    public function reset(): ValidationInterface
974
    {
975
        $this->data         = [];
1,612✔
976
        $this->validated    = [];
1,612✔
977
        $this->rules        = [];
1,612✔
978
        $this->errors       = [];
1,612✔
979
        $this->customErrors = [];
1,612✔
980

981
        return $this;
1,612✔
982
    }
983
}
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