CodeIgniter4 icon indicating copy to clipboard operation
CodeIgniter4 copied to clipboard

Bug: No validation rules for the placeholder: "4".

Open kvdpxne opened this issue 7 months ago • 6 comments

PHP Version

8.4

CodeIgniter4 Version

4.6.1

CodeIgniter4 Installation Method

Composer (using codeigniter4/appstarter)

Which operating systems have you tested for this bug?

Windows

Which server did you use?

apache

Database

No response

What happened?

When validating an array with 5 or more items using a regex_match rule containing curly braces (e.g., \d{4} or \d{6}), CodeIgniter throws a CodeIgniter\Exceptions\LogicException: No validation rules for the placeholder: "4". You must set the validation rules for the field. See <https://codeigniter4.github.io/userguide/libraries/validation.html#validation-placeholders>

My observations:

  • The error only occurs when the data array has ≥5 items (index 4 triggers the bug).
  • The issue stems from CodeIgniter's parser misinterpreting numeric curly braces inside the regex pattern as validation placeholders (e.g., {4}).

I might be misunderstanding how validation placeholders interact with regex patterns. If this is expected behavior, clarification in documentation would be appreciated.

Steps to Reproduce

<?php

namespace placeholders;

use CodeIgniter\Test\CIUnitTestCase;
use Config\Services;

final class RegexTest extends CIUnitTestCase {

  private array $rules = [
    '*.uid' => [
      'label' => 'Order.uid',
      'rules' => [
        'if_exist',
        'required',
        'string',
        'regex_match[/^(\d{4})\/(0[1-9]|1[0-2])\/\d{6}$/]' // Contains {4} and {6}  
      ]
    ]
  ];

  private array $data = [
    ['uid' => '2025/06/000001'],
    ['uid' => '2025/06/000002'],
    ['uid' => '2025/06/000003'],
    ['uid' => '2025/06/000004']
  ];

  public function testLessThat5() {
    $validation = Services::validation();
    $validation->reset();
    $validation->setRules($this->rules);

    // Works with 4 items
    self::assertTrue($validation->run($this->data));
  }

  public function testEqualTo5() {
    $validation = Services::validation();
    $validation->reset();
    $validation->setRules($this->rules);

    // Fails with 5 items (index 4 triggers the bug)
    $largerData = array_merge($this->data, [['uid' => '2025/06/000005']]);

    // Throws CodeIgniter\Exceptions\LogicException
    self::assertTrue($validation->run($largerData));
  }
}

Expected Output

Validation should ignore curly braces inside regex patterns and run successfully regardless of:

  • The presence of {n} in regex patterns.
  • The size of the data array (including arrays with ≥5 items).

Anything else?

Root Cause: The validation parser incorrectly scans regex patterns (delimited by /.../) for placeholders like {4}. When the data array has 5+ items, the index 4 is misinterpreted as a placeholder requiring validation rules.

Workaround: As a temporary solution, you can use a custom validation rule to bypass the parser issue:

// Custom rule
public function valid_uid(string $uid): bool {
  return (bool) preg_match('/^(\d{4})\/(0[1-9]|1[0-2])\/\d{6}$/', $uid);
}

// Rules definition  
'rules' => [  
    'required',  
    'valid_uid' // No regex pattern in string -> no parsing issues  
]  

Although the above custom rule allows avoiding this problem, I think that using such a workaround shouldn't be necessary :/

kvdpxne avatar Jun 07 '25 22:06 kvdpxne

Seems like this is caused by searching for placeholders in the regex_match rule. Placeholders are defined with curly brackets. I'm not sure if we should allow placeholders in the regex_match, but probably not.

michalsn avatar Jun 09 '25 06:06 michalsn

Okay, I finally had some time to look into this and realized I basically just repeated what the OP already pointed out in the report - sorry about that.

This looks like a fairly specific bug that occurs when using the regex_match rule in combination with array validation. Since removing support for placeholders in this rule could introduce a BC break, I think the better solution is to recommend a small change in how the regex is written.

A simple workaround is to avoid using {} quantifiers, like so:

regex_match[/^(\d\d\d\d)\/(0[1-9]|1[0-2])\/\d\d\d\d\d\d$/]

So I believe we should go with this approach and update the user guide to make it clear.

Does anyone have any other ideas?

michalsn avatar Jun 09 '25 15:06 michalsn

I think it should be fixed because when a quantifier shows the min/max like \d{0,5} I'm not sure how to work around that.

The fix I'm thinking is to allow escaping in the rule definition, e.g. regex_match[/^(\d\{0,5\})\.(\d\{4\})/]. Then, when placeholders are parsed, the pattern is adjusted so that the unescaped {} are collected only. Then, after all placeholders are substituted, the escaped {} are now unescaped for use by the rules.

paulbalandan avatar Jun 09 '25 18:06 paulbalandan

Good point about the min/max example - I had forgotten about that. However, I'm not sure about the proposed solution. It requires users to remember escaping rules, makes regex patterns harder to read, and is prone to errors (like forgetting to escape something).

I have come up with two possible alternatives:

  1. Change the placeholder syntax (only for regex_match), to {{placeholder}}
  2. Add a method like setValidPlaceholders() to explicitly define which placeholders should be allowed

The first option would be a breaking change, while the second might introduce some confusion, since it wouldn’t be required and it might not be clear when it should be used.

So far, nothing seems to be the best approach 😅

michalsn avatar Jun 09 '25 20:06 michalsn

Would the confusion on valid placeholder use arise only from regex_match? If yes, why not just a special handling for it? Like skip placeholder filling when the rule is encountered.

paulbalandan avatar Jun 10 '25 00:06 paulbalandan

I actually considered that in my initial reply. The issue is that using placeholders inside regex_match is technically valid. I'm not sure how common that usage is, but introducing special handling could end up breaking someone's app.

That's why, instead of skipping it entirely, I would lean toward introducing a special syntax.

michalsn avatar Jun 10 '25 05:06 michalsn

This issue is now resolved via #9597 and will be included in the 4.7 release.

michalsn avatar Jun 16 '25 05:06 michalsn