Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions system/Validation/Validation.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -168,12 +168,14 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
}

if (strpos($field, '*') !== false) {
$values = array_filter(array_flatten_with_dots($data), static fn ($key) => preg_match(
'/^'
. str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/'))
. '$/',
$key
), ARRAY_FILTER_USE_KEY);
$flattenedArray = array_flatten_with_dots($data);

$values = array_filter(
$flattenedArray,
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

// if keys not found
$values = $values ?: [$field => null];
} else {
Expand DownExpand Up@@ -211,6 +213,20 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
return false;
}

/**
* Returns regex pattern for key with dot array syntax.
*/
private static function getRegex(string $field): string
{
return '/\A'
. str_replace(
['\.\*', '\*\.'],
['\.[^.]+', '[^.]+\.'],
preg_quote($field, '/')
)
. '\z/';
}

/**
* Runs the validation process, returning true or false determining whether
* validation was successful or not.
Expand DownExpand Up@@ -814,9 +830,7 @@ private function retrievePlaceholders(string $rule, array $data): array
*/
public function hasError(string $field): bool
{
$pattern = '/^' . str_replace('\.\*', '\..+', preg_quote($field, '/')) . '$/';

return (bool) preg_grep($pattern, array_keys($this->getErrors()));
return (bool) preg_grep(self::getRegex($field), array_keys($this->getErrors()));
}

/**
Expand All@@ -829,10 +843,11 @@ public function getError(?string $field = null): string
$field = array_key_first($this->rules);
}

$errors = array_filter($this->getErrors(), static fn ($key) => preg_match(
'/^' . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) . '$/',
$key
), ARRAY_FILTER_USE_KEY);
$errors = array_filter(
$this->getErrors(),
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

return $errors === [] ? '' : implode("\n", $errors);
}
Expand Down
66 changes: 56 additions & 10 deletions tests/system/Validation/ValidationTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,17 +1111,17 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$request = new IncomingRequest($config, new URI(), 'php://input', new UserAgent());

$this->validation->setRules([
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.*.name' => 'required',
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.friends.*.name' => 'required',
]);

$this->validation->withRequest($request->withMethod('post'))->run();
$this->assertSame([
'id_user.0' => 'The id_user.* field must contain only numbers.',
'name_user.0' => 'The name_user.* field may only contain alphabetical characters.',
'name_user.2' => 'The name_user.* field may only contain alphabetical characters.',
'contacts.friends.0.name' => 'The contacts.*.name field is required.',
'contacts.friends.0.name' => 'The contacts.friends.*.name field is required.',
], $this->validation->getErrors());

$this->assertSame(
Expand All@@ -1130,8 +1130,8 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$this->validation->getError('name_user.*')
);
$this->assertSame(
'The contacts.*.name field is required.',
$this->validation->getError('contacts.*.name')
'The contacts.friends.*.name field is required.',
$this->validation->getError('contacts.friends.*.name')
);
}

Expand DownExpand Up@@ -1228,17 +1228,17 @@ public function testTranslatedLabelTagReplacement(): void
}

/**
* @dataProvider provideDotNotationOnIfExistRule
* @dataProvider provideIfExistRuleWithAsterisk
*
* @see https://github.com/codeigniter4/CodeIgniter4/issues/4521
*/
public function testDotNotationOnIfExistRule(bool $expected, array $rules, array $data): void
public function testIfExistRuleWithAsterisk(bool $expected, array $rules, array $data): void
{
$actual = $this->validation->setRules($rules)->run($data);
$this->assertSame($expected, $actual);
}

public static function provideDotNotationOnIfExistRule(): iterable
public static function provideIfExistRuleWithAsterisk(): iterable
{
yield 'dot-on-end-fail' => [
false,
Expand DownExpand Up@@ -1613,7 +1613,7 @@ public function testRuleWithLeadingAsterisk(): void
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/5942
*/
public function testRequireWithoutWithWildCard(): void
public function testRequireWithoutWithAsterisk(): void
{
$data = [
'a' => [
Expand All@@ -1631,4 +1631,50 @@ public function testRequireWithoutWithWildCard(): void
$this->validation->getError('a.1.c')
);
}

/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/8128
*/
public function testRuleWithAsteriskToMultiDimensionalArray(): void
{
$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
[
'name' => 'Fred Flinstone',
],
[
'name' => 'Wilma',
],
],
],
],
];

$this->validation->setRules(
['contacts.just.friends.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
[
'contacts.just.friends.0.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
'contacts.just.friends.1.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
],
$this->validation->getErrors()
);

$this->validation->reset();
$this->validation->setRules(
['contacts.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
// The data for `contacts.*.name` does not exist. So it is interpreted
// as `null`, and this error message returns.
['contacts.*.name' => 'The contacts.*.name field is required.'],
$this->validation->getErrors()
);
}
}
7 changes: 7 additions & 0 deletions user_guide_src/source/changelogs/v4.4.4.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,13 @@ Release Date: Unreleased
BREAKING
********

Validation with Dot Array Syntax
================================

A validation rule with the wildcard ``*`` now validates only data in correct
dimensions as "dot array syntax".
See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>` for details.

***************
Message Changes
***************
Expand Down
19 changes: 19 additions & 0 deletions user_guide_src/source/installation/upgrade_444.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,25 @@ Mandatory File Changes
Breaking Changes
****************

.. _upgrade-444-validation-with-dot-array-syntax:

Validation with Dot Array Syntax
================================

If you are using :ref:`dot array syntax <validation-dot-array-syntax>` in validation
rules, a bug where ``*`` would validate data in incorrect dimensions has been fixed.

In previous versions, the rule key ``contacts.*.name`` captured data with any
level like ``contacts.*.name``, ``contacts.*.*.name``, ``contacts.*.*.*.name``,
etc., incorrectly.

The following code explains details:

.. literalinclude:: upgrade_444/001.php
:lines: 2-

If you have code that depends on the bug, fix the the rule key.

*********************
Breaking Enhancements
*********************
Expand Down
38 changes: 38 additions & 0 deletions user_guide_src/source/installation/upgrade_444/001.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
<?php

use Config\Services;

$validation = Services::validation();

$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
['name' => 'SATO Taro'],
['name' => 'Li Ming'],
['name' => 'Heinz Müller'],
],
],
],
];

$validation->setRules(
['contacts.*.name' => 'required|max_length[8]']
);

$validation->run($data); // false

d($validation->getErrors());
/*
Before: Captured `contacts.*.*.*.name` incorrectly.
[
contacts.just.friends.0.name => "The contacts.*.name field cannot exceed 8 characters in length.",
contacts.just.friends.2.name => "The contacts.*.name field cannot exceed 8 characters in length.",
]

After: Captures no data for `contacts.*.name`.
[
contacts.*.name => string (38) "The contacts.*.name field is required.",
]
*/
15 changes: 11 additions & 4 deletions user_guide_src/source/libraries/validation.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -314,6 +314,8 @@ To give a labeled error message you can set up as:
.. note:: ``setRules()`` will overwrite any rules that were set previously. To add more than one
rule to an existing set of rules, use ``setRule()`` multiple times.

.. _validation-dot-array-syntax:

Setting Rules for Array Data
============================

Expand All@@ -328,6 +330,10 @@ You can use the ``*`` wildcard symbol to match any one level of the array:
.. literalinclude:: validation/010.php
:lines: 2-

.. note:: Prior to v4.4.4, due to a bug, the wildcard ``*`` validated data in incorrect
dimensions. See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>`
for details.

"dot array syntax" can also be useful when you have single dimension array data.
For example, data returned by multi select dropdown:

Expand DownExpand Up@@ -591,7 +597,7 @@ If you need to retrieve all error messages for failed fields, you can use the ``

If no errors exist, an empty array will be returned.

When using a wildcard, the error will point to a specific field, replacing the asterisk with the appropriate key/keys::
When using a wildcard (``*``), the error will point to a specific field, replacing the asterisk with the appropriate key/keys::

// for data
'contacts' => [
Expand All@@ -606,10 +612,10 @@ When using a wildcard, the error will point to a specific field, replacing the a
]

// rule
'contacts.*.name' => 'required'
'contacts.friends.*.name' => 'required'

// error will be
'contacts.friends.1.name' => 'The contacts.*.name field is required.'
'contacts.friends.1.name' => 'The contacts.friends.*.name field is required.'

Getting a Single Error
======================
Expand DownExpand Up@@ -830,7 +836,8 @@ alpha_numeric_punct No Fails if field contains anything other than
alphanumeric, space, or this limited set of
punctuation characters: ``~`` (tilde),
``!`` (exclamation), ``#`` (number),
``$`` (dollar), ``% (percent), & (ampersand),
``$`` (dollar), ``%`` (percent),
``&`` (ampersand),
``*`` (asterisk), ``-`` (dash),
``_`` (underscore), ``+`` (plus),
``=`` (equals), ``|`` (vertical bar),
Expand Down
7 changes: 1 addition & 6 deletions user_guide_src/source/libraries/validation/009.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@
* The data to test:
* [
* 'contacts' => [
* 'name' => 'Joe Smith',
* 'name' => 'Joe Smith',
* 'friends' => [
* [
* 'name' => 'Fred Flinstone',
Expand All@@ -21,8 +21,3 @@
$validation->setRules([
'contacts.name' => 'required|max_length[60]',
]);

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.friends.name' => 'required|max_length[60]',
]);
2 changes: 1 addition & 1 deletion user_guide_src/source/libraries/validation/010.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.*.name' => 'required|max_length[60]',
'contacts.friends.*.name' => 'required|max_length[60]',
]);
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions system/Validation/Validation.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -168,12 +168,14 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
}

if (strpos($field, '*') !== false) {
$values = array_filter(array_flatten_with_dots($data), static fn ($key) => preg_match(
'/^'
. str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/'))
. '$/',
$key
), ARRAY_FILTER_USE_KEY);
$flattenedArray = array_flatten_with_dots($data);

$values = array_filter(
$flattenedArray,
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

// if keys not found
$values = $values ?: [$field => null];
} else {
Expand DownExpand Up@@ -211,6 +213,20 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
return false;
}

/**
* Returns regex pattern for key with dot array syntax.
*/
private static function getRegex(string $field): string
{
return '/\A'
. str_replace(
['\.\*', '\*\.'],
['\.[^.]+', '[^.]+\.'],
preg_quote($field, '/')
)
. '\z/';
}

/**
* Runs the validation process, returning true or false determining whether
* validation was successful or not.
Expand DownExpand Up@@ -814,9 +830,7 @@ private function retrievePlaceholders(string $rule, array $data): array
*/
public function hasError(string $field): bool
{
$pattern = '/^' . str_replace('\.\*', '\..+', preg_quote($field, '/')) . '$/';

return (bool) preg_grep($pattern, array_keys($this->getErrors()));
return (bool) preg_grep(self::getRegex($field), array_keys($this->getErrors()));
}

/**
Expand All@@ -829,10 +843,11 @@ public function getError(?string $field = null): string
$field = array_key_first($this->rules);
}

$errors = array_filter($this->getErrors(), static fn ($key) => preg_match(
'/^' . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) . '$/',
$key
), ARRAY_FILTER_USE_KEY);
$errors = array_filter(
$this->getErrors(),
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

return $errors === [] ? '' : implode("\n", $errors);
}
Expand Down
66 changes: 56 additions & 10 deletions tests/system/Validation/ValidationTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,17 +1111,17 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$request = new IncomingRequest($config, new URI(), 'php://input', new UserAgent());

$this->validation->setRules([
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.*.name' => 'required',
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.friends.*.name' => 'required',
]);

$this->validation->withRequest($request->withMethod('post'))->run();
$this->assertSame([
'id_user.0' => 'The id_user.* field must contain only numbers.',
'name_user.0' => 'The name_user.* field may only contain alphabetical characters.',
'name_user.2' => 'The name_user.* field may only contain alphabetical characters.',
'contacts.friends.0.name' => 'The contacts.*.name field is required.',
'contacts.friends.0.name' => 'The contacts.friends.*.name field is required.',
], $this->validation->getErrors());

$this->assertSame(
Expand All@@ -1130,8 +1130,8 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$this->validation->getError('name_user.*')
);
$this->assertSame(
'The contacts.*.name field is required.',
$this->validation->getError('contacts.*.name')
'The contacts.friends.*.name field is required.',
$this->validation->getError('contacts.friends.*.name')
);
}

Expand DownExpand Up@@ -1228,17 +1228,17 @@ public function testTranslatedLabelTagReplacement(): void
}

/**
* @dataProvider provideDotNotationOnIfExistRule
* @dataProvider provideIfExistRuleWithAsterisk
*
* @see https://github.com/codeigniter4/CodeIgniter4/issues/4521
*/
public function testDotNotationOnIfExistRule(bool $expected, array $rules, array $data): void
public function testIfExistRuleWithAsterisk(bool $expected, array $rules, array $data): void
{
$actual = $this->validation->setRules($rules)->run($data);
$this->assertSame($expected, $actual);
}

public static function provideDotNotationOnIfExistRule(): iterable
public static function provideIfExistRuleWithAsterisk(): iterable
{
yield 'dot-on-end-fail' => [
false,
Expand DownExpand Up@@ -1613,7 +1613,7 @@ public function testRuleWithLeadingAsterisk(): void
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/5942
*/
public function testRequireWithoutWithWildCard(): void
public function testRequireWithoutWithAsterisk(): void
{
$data = [
'a' => [
Expand All@@ -1631,4 +1631,50 @@ public function testRequireWithoutWithWildCard(): void
$this->validation->getError('a.1.c')
);
}

/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/8128
*/
public function testRuleWithAsteriskToMultiDimensionalArray(): void
{
$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
[
'name' => 'Fred Flinstone',
],
[
'name' => 'Wilma',
],
],
],
],
];

$this->validation->setRules(
['contacts.just.friends.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
[
'contacts.just.friends.0.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
'contacts.just.friends.1.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
],
$this->validation->getErrors()
);

$this->validation->reset();
$this->validation->setRules(
['contacts.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
// The data for `contacts.*.name` does not exist. So it is interpreted
// as `null`, and this error message returns.
['contacts.*.name' => 'The contacts.*.name field is required.'],
$this->validation->getErrors()
);
}
}
7 changes: 7 additions & 0 deletions user_guide_src/source/changelogs/v4.4.4.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,13 @@ Release Date: Unreleased
BREAKING
********

Validation with Dot Array Syntax
================================

A validation rule with the wildcard ``*`` now validates only data in correct
dimensions as "dot array syntax".
See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>` for details.

***************
Message Changes
***************
Expand Down
19 changes: 19 additions & 0 deletions user_guide_src/source/installation/upgrade_444.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,25 @@ Mandatory File Changes
Breaking Changes
****************

.. _upgrade-444-validation-with-dot-array-syntax:

Validation with Dot Array Syntax
================================

If you are using :ref:`dot array syntax <validation-dot-array-syntax>` in validation
rules, a bug where ``*`` would validate data in incorrect dimensions has been fixed.

In previous versions, the rule key ``contacts.*.name`` captured data with any
level like ``contacts.*.name``, ``contacts.*.*.name``, ``contacts.*.*.*.name``,
etc., incorrectly.

The following code explains details:

.. literalinclude:: upgrade_444/001.php
:lines: 2-

If you have code that depends on the bug, fix the the rule key.

*********************
Breaking Enhancements
*********************
Expand Down
38 changes: 38 additions & 0 deletions user_guide_src/source/installation/upgrade_444/001.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
<?php

use Config\Services;

$validation = Services::validation();

$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
['name' => 'SATO Taro'],
['name' => 'Li Ming'],
['name' => 'Heinz Müller'],
],
],
],
];

$validation->setRules(
['contacts.*.name' => 'required|max_length[8]']
);

$validation->run($data); // false

d($validation->getErrors());
/*
Before: Captured `contacts.*.*.*.name` incorrectly.
[
contacts.just.friends.0.name => "The contacts.*.name field cannot exceed 8 characters in length.",
contacts.just.friends.2.name => "The contacts.*.name field cannot exceed 8 characters in length.",
]

After: Captures no data for `contacts.*.name`.
[
contacts.*.name => string (38) "The contacts.*.name field is required.",
]
*/
15 changes: 11 additions & 4 deletions user_guide_src/source/libraries/validation.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -314,6 +314,8 @@ To give a labeled error message you can set up as:
.. note:: ``setRules()`` will overwrite any rules that were set previously. To add more than one
rule to an existing set of rules, use ``setRule()`` multiple times.

.. _validation-dot-array-syntax:

Setting Rules for Array Data
============================

Expand All@@ -328,6 +330,10 @@ You can use the ``*`` wildcard symbol to match any one level of the array:
.. literalinclude:: validation/010.php
:lines: 2-

.. note:: Prior to v4.4.4, due to a bug, the wildcard ``*`` validated data in incorrect
dimensions. See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>`
for details.

"dot array syntax" can also be useful when you have single dimension array data.
For example, data returned by multi select dropdown:

Expand DownExpand Up@@ -591,7 +597,7 @@ If you need to retrieve all error messages for failed fields, you can use the ``

If no errors exist, an empty array will be returned.

When using a wildcard, the error will point to a specific field, replacing the asterisk with the appropriate key/keys::
When using a wildcard (``*``), the error will point to a specific field, replacing the asterisk with the appropriate key/keys::

// for data
'contacts' => [
Expand All@@ -606,10 +612,10 @@ When using a wildcard, the error will point to a specific field, replacing the a
]

// rule
'contacts.*.name' => 'required'
'contacts.friends.*.name' => 'required'

// error will be
'contacts.friends.1.name' => 'The contacts.*.name field is required.'
'contacts.friends.1.name' => 'The contacts.friends.*.name field is required.'

Getting a Single Error
======================
Expand DownExpand Up@@ -830,7 +836,8 @@ alpha_numeric_punct No Fails if field contains anything other than
alphanumeric, space, or this limited set of
punctuation characters: ``~`` (tilde),
``!`` (exclamation), ``#`` (number),
``$`` (dollar), ``% (percent), & (ampersand),
``$`` (dollar), ``%`` (percent),
``&`` (ampersand),
``*`` (asterisk), ``-`` (dash),
``_`` (underscore), ``+`` (plus),
``=`` (equals), ``|`` (vertical bar),
Expand Down
7 changes: 1 addition & 6 deletions user_guide_src/source/libraries/validation/009.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@
* The data to test:
* [
* 'contacts' => [
* 'name' => 'Joe Smith',
* 'name' => 'Joe Smith',
* 'friends' => [
* [
* 'name' => 'Fred Flinstone',
Expand All@@ -21,8 +21,3 @@
$validation->setRules([
'contacts.name' => 'required|max_length[60]',
]);

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.friends.name' => 'required|max_length[60]',
]);
2 changes: 1 addition & 1 deletion user_guide_src/source/libraries/validation/010.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.*.name' => 'required|max_length[60]',
'contacts.friends.*.name' => 'required|max_length[60]',
]);
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions system/Validation/Validation.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -168,12 +168,14 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
}

if (strpos($field, '*') !== false) {
$values = array_filter(array_flatten_with_dots($data), static fn ($key) => preg_match(
'/^'
. str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/'))
. '$/',
$key
), ARRAY_FILTER_USE_KEY);
$flattenedArray = array_flatten_with_dots($data);

$values = array_filter(
$flattenedArray,
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

// if keys not found
$values = $values ?: [$field => null];
} else {
Expand DownExpand Up@@ -211,6 +213,20 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
return false;
}

/**
* Returns regex pattern for key with dot array syntax.
*/
private static function getRegex(string $field): string
{
return '/\A'
. str_replace(
['\.\*', '\*\.'],
['\.[^.]+', '[^.]+\.'],
preg_quote($field, '/')
)
. '\z/';
}

/**
* Runs the validation process, returning true or false determining whether
* validation was successful or not.
Expand DownExpand Up@@ -814,9 +830,7 @@ private function retrievePlaceholders(string $rule, array $data): array
*/
public function hasError(string $field): bool
{
$pattern = '/^' . str_replace('\.\*', '\..+', preg_quote($field, '/')) . '$/';

return (bool) preg_grep($pattern, array_keys($this->getErrors()));
return (bool) preg_grep(self::getRegex($field), array_keys($this->getErrors()));
}

/**
Expand All@@ -829,10 +843,11 @@ public function getError(?string $field = null): string
$field = array_key_first($this->rules);
}

$errors = array_filter($this->getErrors(), static fn ($key) => preg_match(
'/^' . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) . '$/',
$key
), ARRAY_FILTER_USE_KEY);
$errors = array_filter(
$this->getErrors(),
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

return $errors === [] ? '' : implode("\n", $errors);
}
Expand Down
66 changes: 56 additions & 10 deletions tests/system/Validation/ValidationTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,17 +1111,17 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$request = new IncomingRequest($config, new URI(), 'php://input', new UserAgent());

$this->validation->setRules([
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.*.name' => 'required',
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.friends.*.name' => 'required',
]);

$this->validation->withRequest($request->withMethod('post'))->run();
$this->assertSame([
'id_user.0' => 'The id_user.* field must contain only numbers.',
'name_user.0' => 'The name_user.* field may only contain alphabetical characters.',
'name_user.2' => 'The name_user.* field may only contain alphabetical characters.',
'contacts.friends.0.name' => 'The contacts.*.name field is required.',
'contacts.friends.0.name' => 'The contacts.friends.*.name field is required.',
], $this->validation->getErrors());

$this->assertSame(
Expand All@@ -1130,8 +1130,8 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$this->validation->getError('name_user.*')
);
$this->assertSame(
'The contacts.*.name field is required.',
$this->validation->getError('contacts.*.name')
'The contacts.friends.*.name field is required.',
$this->validation->getError('contacts.friends.*.name')
);
}

Expand DownExpand Up@@ -1228,17 +1228,17 @@ public function testTranslatedLabelTagReplacement(): void
}

/**
* @dataProvider provideDotNotationOnIfExistRule
* @dataProvider provideIfExistRuleWithAsterisk
*
* @see https://github.com/codeigniter4/CodeIgniter4/issues/4521
*/
public function testDotNotationOnIfExistRule(bool $expected, array $rules, array $data): void
public function testIfExistRuleWithAsterisk(bool $expected, array $rules, array $data): void
{
$actual = $this->validation->setRules($rules)->run($data);
$this->assertSame($expected, $actual);
}

public static function provideDotNotationOnIfExistRule(): iterable
public static function provideIfExistRuleWithAsterisk(): iterable
{
yield 'dot-on-end-fail' => [
false,
Expand DownExpand Up@@ -1613,7 +1613,7 @@ public function testRuleWithLeadingAsterisk(): void
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/5942
*/
public function testRequireWithoutWithWildCard(): void
public function testRequireWithoutWithAsterisk(): void
{
$data = [
'a' => [
Expand All@@ -1631,4 +1631,50 @@ public function testRequireWithoutWithWildCard(): void
$this->validation->getError('a.1.c')
);
}

/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/8128
*/
public function testRuleWithAsteriskToMultiDimensionalArray(): void
{
$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
[
'name' => 'Fred Flinstone',
],
[
'name' => 'Wilma',
],
],
],
],
];

$this->validation->setRules(
['contacts.just.friends.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
[
'contacts.just.friends.0.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
'contacts.just.friends.1.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
],
$this->validation->getErrors()
);

$this->validation->reset();
$this->validation->setRules(
['contacts.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
// The data for `contacts.*.name` does not exist. So it is interpreted
// as `null`, and this error message returns.
['contacts.*.name' => 'The contacts.*.name field is required.'],
$this->validation->getErrors()
);
}
}
7 changes: 7 additions & 0 deletions user_guide_src/source/changelogs/v4.4.4.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,13 @@ Release Date: Unreleased
BREAKING
********

Validation with Dot Array Syntax
================================

A validation rule with the wildcard ``*`` now validates only data in correct
dimensions as "dot array syntax".
See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>` for details.

***************
Message Changes
***************
Expand Down
19 changes: 19 additions & 0 deletions user_guide_src/source/installation/upgrade_444.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,25 @@ Mandatory File Changes
Breaking Changes
****************

.. _upgrade-444-validation-with-dot-array-syntax:

Validation with Dot Array Syntax
================================

If you are using :ref:`dot array syntax <validation-dot-array-syntax>` in validation
rules, a bug where ``*`` would validate data in incorrect dimensions has been fixed.

In previous versions, the rule key ``contacts.*.name`` captured data with any
level like ``contacts.*.name``, ``contacts.*.*.name``, ``contacts.*.*.*.name``,
etc., incorrectly.

The following code explains details:

.. literalinclude:: upgrade_444/001.php
:lines: 2-

If you have code that depends on the bug, fix the the rule key.

*********************
Breaking Enhancements
*********************
Expand Down
38 changes: 38 additions & 0 deletions user_guide_src/source/installation/upgrade_444/001.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
<?php

use Config\Services;

$validation = Services::validation();

$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
['name' => 'SATO Taro'],
['name' => 'Li Ming'],
['name' => 'Heinz Müller'],
],
],
],
];

$validation->setRules(
['contacts.*.name' => 'required|max_length[8]']
);

$validation->run($data); // false

d($validation->getErrors());
/*
Before: Captured `contacts.*.*.*.name` incorrectly.
[
contacts.just.friends.0.name => "The contacts.*.name field cannot exceed 8 characters in length.",
contacts.just.friends.2.name => "The contacts.*.name field cannot exceed 8 characters in length.",
]

After: Captures no data for `contacts.*.name`.
[
contacts.*.name => string (38) "The contacts.*.name field is required.",
]
*/
15 changes: 11 additions & 4 deletions user_guide_src/source/libraries/validation.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -314,6 +314,8 @@ To give a labeled error message you can set up as:
.. note:: ``setRules()`` will overwrite any rules that were set previously. To add more than one
rule to an existing set of rules, use ``setRule()`` multiple times.

.. _validation-dot-array-syntax:

Setting Rules for Array Data
============================

Expand All@@ -328,6 +330,10 @@ You can use the ``*`` wildcard symbol to match any one level of the array:
.. literalinclude:: validation/010.php
:lines: 2-

.. note:: Prior to v4.4.4, due to a bug, the wildcard ``*`` validated data in incorrect
dimensions. See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>`
for details.

"dot array syntax" can also be useful when you have single dimension array data.
For example, data returned by multi select dropdown:

Expand DownExpand Up@@ -591,7 +597,7 @@ If you need to retrieve all error messages for failed fields, you can use the ``

If no errors exist, an empty array will be returned.

When using a wildcard, the error will point to a specific field, replacing the asterisk with the appropriate key/keys::
When using a wildcard (``*``), the error will point to a specific field, replacing the asterisk with the appropriate key/keys::

// for data
'contacts' => [
Expand All@@ -606,10 +612,10 @@ When using a wildcard, the error will point to a specific field, replacing the a
]

// rule
'contacts.*.name' => 'required'
'contacts.friends.*.name' => 'required'

// error will be
'contacts.friends.1.name' => 'The contacts.*.name field is required.'
'contacts.friends.1.name' => 'The contacts.friends.*.name field is required.'

Getting a Single Error
======================
Expand DownExpand Up@@ -830,7 +836,8 @@ alpha_numeric_punct No Fails if field contains anything other than
alphanumeric, space, or this limited set of
punctuation characters: ``~`` (tilde),
``!`` (exclamation), ``#`` (number),
``$`` (dollar), ``% (percent), & (ampersand),
``$`` (dollar), ``%`` (percent),
``&`` (ampersand),
``*`` (asterisk), ``-`` (dash),
``_`` (underscore), ``+`` (plus),
``=`` (equals), ``|`` (vertical bar),
Expand Down
7 changes: 1 addition & 6 deletions user_guide_src/source/libraries/validation/009.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@
* The data to test:
* [
* 'contacts' => [
* 'name' => 'Joe Smith',
* 'name' => 'Joe Smith',
* 'friends' => [
* [
* 'name' => 'Fred Flinstone',
Expand All@@ -21,8 +21,3 @@
$validation->setRules([
'contacts.name' => 'required|max_length[60]',
]);

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.friends.name' => 'required|max_length[60]',
]);
2 changes: 1 addition & 1 deletion user_guide_src/source/libraries/validation/010.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.*.name' => 'required|max_length[60]',
'contacts.friends.*.name' => 'required|max_length[60]',
]);
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions system/Validation/Validation.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -168,12 +168,14 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
}

if (strpos($field, '*') !== false) {
$values = array_filter(array_flatten_with_dots($data), static fn ($key) => preg_match(
'/^'
. str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/'))
. '$/',
$key
), ARRAY_FILTER_USE_KEY);
$flattenedArray = array_flatten_with_dots($data);

$values = array_filter(
$flattenedArray,
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

// if keys not found
$values = $values ?: [$field => null];
} else {
Expand DownExpand Up@@ -211,6 +213,20 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
return false;
}

/**
* Returns regex pattern for key with dot array syntax.
*/
private static function getRegex(string $field): string
{
return '/\A'
. str_replace(
['\.\*', '\*\.'],
['\.[^.]+', '[^.]+\.'],
preg_quote($field, '/')
)
. '\z/';
}

/**
* Runs the validation process, returning true or false determining whether
* validation was successful or not.
Expand DownExpand Up@@ -814,9 +830,7 @@ private function retrievePlaceholders(string $rule, array $data): array
*/
public function hasError(string $field): bool
{
$pattern = '/^' . str_replace('\.\*', '\..+', preg_quote($field, '/')) . '$/';

return (bool) preg_grep($pattern, array_keys($this->getErrors()));
return (bool) preg_grep(self::getRegex($field), array_keys($this->getErrors()));
}

/**
Expand All@@ -829,10 +843,11 @@ public function getError(?string $field = null): string
$field = array_key_first($this->rules);
}

$errors = array_filter($this->getErrors(), static fn ($key) => preg_match(
'/^' . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) . '$/',
$key
), ARRAY_FILTER_USE_KEY);
$errors = array_filter(
$this->getErrors(),
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

return $errors === [] ? '' : implode("\n", $errors);
}
Expand Down
66 changes: 56 additions & 10 deletions tests/system/Validation/ValidationTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,17 +1111,17 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$request = new IncomingRequest($config, new URI(), 'php://input', new UserAgent());

$this->validation->setRules([
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.*.name' => 'required',
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.friends.*.name' => 'required',
]);

$this->validation->withRequest($request->withMethod('post'))->run();
$this->assertSame([
'id_user.0' => 'The id_user.* field must contain only numbers.',
'name_user.0' => 'The name_user.* field may only contain alphabetical characters.',
'name_user.2' => 'The name_user.* field may only contain alphabetical characters.',
'contacts.friends.0.name' => 'The contacts.*.name field is required.',
'contacts.friends.0.name' => 'The contacts.friends.*.name field is required.',
], $this->validation->getErrors());

$this->assertSame(
Expand All@@ -1130,8 +1130,8 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$this->validation->getError('name_user.*')
);
$this->assertSame(
'The contacts.*.name field is required.',
$this->validation->getError('contacts.*.name')
'The contacts.friends.*.name field is required.',
$this->validation->getError('contacts.friends.*.name')
);
}

Expand DownExpand Up@@ -1228,17 +1228,17 @@ public function testTranslatedLabelTagReplacement(): void
}

/**
* @dataProvider provideDotNotationOnIfExistRule
* @dataProvider provideIfExistRuleWithAsterisk
*
* @see https://github.com/codeigniter4/CodeIgniter4/issues/4521
*/
public function testDotNotationOnIfExistRule(bool $expected, array $rules, array $data): void
public function testIfExistRuleWithAsterisk(bool $expected, array $rules, array $data): void
{
$actual = $this->validation->setRules($rules)->run($data);
$this->assertSame($expected, $actual);
}

public static function provideDotNotationOnIfExistRule(): iterable
public static function provideIfExistRuleWithAsterisk(): iterable
{
yield 'dot-on-end-fail' => [
false,
Expand DownExpand Up@@ -1613,7 +1613,7 @@ public function testRuleWithLeadingAsterisk(): void
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/5942
*/
public function testRequireWithoutWithWildCard(): void
public function testRequireWithoutWithAsterisk(): void
{
$data = [
'a' => [
Expand All@@ -1631,4 +1631,50 @@ public function testRequireWithoutWithWildCard(): void
$this->validation->getError('a.1.c')
);
}

/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/8128
*/
public function testRuleWithAsteriskToMultiDimensionalArray(): void
{
$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
[
'name' => 'Fred Flinstone',
],
[
'name' => 'Wilma',
],
],
],
],
];

$this->validation->setRules(
['contacts.just.friends.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
[
'contacts.just.friends.0.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
'contacts.just.friends.1.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
],
$this->validation->getErrors()
);

$this->validation->reset();
$this->validation->setRules(
['contacts.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
// The data for `contacts.*.name` does not exist. So it is interpreted
// as `null`, and this error message returns.
['contacts.*.name' => 'The contacts.*.name field is required.'],
$this->validation->getErrors()
);
}
}
7 changes: 7 additions & 0 deletions user_guide_src/source/changelogs/v4.4.4.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,13 @@ Release Date: Unreleased
BREAKING
********

Validation with Dot Array Syntax
================================

A validation rule with the wildcard ``*`` now validates only data in correct
dimensions as "dot array syntax".
See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>` for details.

***************
Message Changes
***************
Expand Down
19 changes: 19 additions & 0 deletions user_guide_src/source/installation/upgrade_444.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,25 @@ Mandatory File Changes
Breaking Changes
****************

.. _upgrade-444-validation-with-dot-array-syntax:

Validation with Dot Array Syntax
================================

If you are using :ref:`dot array syntax <validation-dot-array-syntax>` in validation
rules, a bug where ``*`` would validate data in incorrect dimensions has been fixed.

In previous versions, the rule key ``contacts.*.name`` captured data with any
level like ``contacts.*.name``, ``contacts.*.*.name``, ``contacts.*.*.*.name``,
etc., incorrectly.

The following code explains details:

.. literalinclude:: upgrade_444/001.php
:lines: 2-

If you have code that depends on the bug, fix the the rule key.

*********************
Breaking Enhancements
*********************
Expand Down
38 changes: 38 additions & 0 deletions user_guide_src/source/installation/upgrade_444/001.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
<?php

use Config\Services;

$validation = Services::validation();

$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
['name' => 'SATO Taro'],
['name' => 'Li Ming'],
['name' => 'Heinz Müller'],
],
],
],
];

$validation->setRules(
['contacts.*.name' => 'required|max_length[8]']
);

$validation->run($data); // false

d($validation->getErrors());
/*
Before: Captured `contacts.*.*.*.name` incorrectly.
[
contacts.just.friends.0.name => "The contacts.*.name field cannot exceed 8 characters in length.",
contacts.just.friends.2.name => "The contacts.*.name field cannot exceed 8 characters in length.",
]

After: Captures no data for `contacts.*.name`.
[
contacts.*.name => string (38) "The contacts.*.name field is required.",
]
*/
15 changes: 11 additions & 4 deletions user_guide_src/source/libraries/validation.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -314,6 +314,8 @@ To give a labeled error message you can set up as:
.. note:: ``setRules()`` will overwrite any rules that were set previously. To add more than one
rule to an existing set of rules, use ``setRule()`` multiple times.

.. _validation-dot-array-syntax:

Setting Rules for Array Data
============================

Expand All@@ -328,6 +330,10 @@ You can use the ``*`` wildcard symbol to match any one level of the array:
.. literalinclude:: validation/010.php
:lines: 2-

.. note:: Prior to v4.4.4, due to a bug, the wildcard ``*`` validated data in incorrect
dimensions. See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>`
for details.

"dot array syntax" can also be useful when you have single dimension array data.
For example, data returned by multi select dropdown:

Expand DownExpand Up@@ -591,7 +597,7 @@ If you need to retrieve all error messages for failed fields, you can use the ``

If no errors exist, an empty array will be returned.

When using a wildcard, the error will point to a specific field, replacing the asterisk with the appropriate key/keys::
When using a wildcard (``*``), the error will point to a specific field, replacing the asterisk with the appropriate key/keys::

// for data
'contacts' => [
Expand All@@ -606,10 +612,10 @@ When using a wildcard, the error will point to a specific field, replacing the a
]

// rule
'contacts.*.name' => 'required'
'contacts.friends.*.name' => 'required'

// error will be
'contacts.friends.1.name' => 'The contacts.*.name field is required.'
'contacts.friends.1.name' => 'The contacts.friends.*.name field is required.'

Getting a Single Error
======================
Expand DownExpand Up@@ -830,7 +836,8 @@ alpha_numeric_punct No Fails if field contains anything other than
alphanumeric, space, or this limited set of
punctuation characters: ``~`` (tilde),
``!`` (exclamation), ``#`` (number),
``$`` (dollar), ``% (percent), & (ampersand),
``$`` (dollar), ``%`` (percent),
``&`` (ampersand),
``*`` (asterisk), ``-`` (dash),
``_`` (underscore), ``+`` (plus),
``=`` (equals), ``|`` (vertical bar),
Expand Down
7 changes: 1 addition & 6 deletions user_guide_src/source/libraries/validation/009.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@
* The data to test:
* [
* 'contacts' => [
* 'name' => 'Joe Smith',
* 'name' => 'Joe Smith',
* 'friends' => [
* [
* 'name' => 'Fred Flinstone',
Expand All@@ -21,8 +21,3 @@
$validation->setRules([
'contacts.name' => 'required|max_length[60]',
]);

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.friends.name' => 'required|max_length[60]',
]);
2 changes: 1 addition & 1 deletion user_guide_src/source/libraries/validation/010.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.*.name' => 'required|max_length[60]',
'contacts.friends.*.name' => 'required|max_length[60]',
]);
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions system/Validation/Validation.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -168,12 +168,14 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
}

if (strpos($field, '*') !== false) {
$values = array_filter(array_flatten_with_dots($data), static fn ($key) => preg_match(
'/^'
. str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/'))
. '$/',
$key
), ARRAY_FILTER_USE_KEY);
$flattenedArray = array_flatten_with_dots($data);

$values = array_filter(
$flattenedArray,
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

// if keys not found
$values = $values ?: [$field => null];
} else {
Expand DownExpand Up@@ -211,6 +213,20 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
return false;
}

/**
* Returns regex pattern for key with dot array syntax.
*/
private static function getRegex(string $field): string
{
return '/\A'
. str_replace(
['\.\*', '\*\.'],
['\.[^.]+', '[^.]+\.'],
preg_quote($field, '/')
)
. '\z/';
}

/**
* Runs the validation process, returning true or false determining whether
* validation was successful or not.
Expand DownExpand Up@@ -814,9 +830,7 @@ private function retrievePlaceholders(string $rule, array $data): array
*/
public function hasError(string $field): bool
{
$pattern = '/^' . str_replace('\.\*', '\..+', preg_quote($field, '/')) . '$/';

return (bool) preg_grep($pattern, array_keys($this->getErrors()));
return (bool) preg_grep(self::getRegex($field), array_keys($this->getErrors()));
}

/**
Expand All@@ -829,10 +843,11 @@ public function getError(?string $field = null): string
$field = array_key_first($this->rules);
}

$errors = array_filter($this->getErrors(), static fn ($key) => preg_match(
'/^' . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) . '$/',
$key
), ARRAY_FILTER_USE_KEY);
$errors = array_filter(
$this->getErrors(),
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

return $errors === [] ? '' : implode("\n", $errors);
}
Expand Down
66 changes: 56 additions & 10 deletions tests/system/Validation/ValidationTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,17 +1111,17 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$request = new IncomingRequest($config, new URI(), 'php://input', new UserAgent());

$this->validation->setRules([
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.*.name' => 'required',
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.friends.*.name' => 'required',
]);

$this->validation->withRequest($request->withMethod('post'))->run();
$this->assertSame([
'id_user.0' => 'The id_user.* field must contain only numbers.',
'name_user.0' => 'The name_user.* field may only contain alphabetical characters.',
'name_user.2' => 'The name_user.* field may only contain alphabetical characters.',
'contacts.friends.0.name' => 'The contacts.*.name field is required.',
'contacts.friends.0.name' => 'The contacts.friends.*.name field is required.',
], $this->validation->getErrors());

$this->assertSame(
Expand All@@ -1130,8 +1130,8 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$this->validation->getError('name_user.*')
);
$this->assertSame(
'The contacts.*.name field is required.',
$this->validation->getError('contacts.*.name')
'The contacts.friends.*.name field is required.',
$this->validation->getError('contacts.friends.*.name')
);
}

Expand DownExpand Up@@ -1228,17 +1228,17 @@ public function testTranslatedLabelTagReplacement(): void
}

/**
* @dataProvider provideDotNotationOnIfExistRule
* @dataProvider provideIfExistRuleWithAsterisk
*
* @see https://github.com/codeigniter4/CodeIgniter4/issues/4521
*/
public function testDotNotationOnIfExistRule(bool $expected, array $rules, array $data): void
public function testIfExistRuleWithAsterisk(bool $expected, array $rules, array $data): void
{
$actual = $this->validation->setRules($rules)->run($data);
$this->assertSame($expected, $actual);
}

public static function provideDotNotationOnIfExistRule(): iterable
public static function provideIfExistRuleWithAsterisk(): iterable
{
yield 'dot-on-end-fail' => [
false,
Expand DownExpand Up@@ -1613,7 +1613,7 @@ public function testRuleWithLeadingAsterisk(): void
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/5942
*/
public function testRequireWithoutWithWildCard(): void
public function testRequireWithoutWithAsterisk(): void
{
$data = [
'a' => [
Expand All@@ -1631,4 +1631,50 @@ public function testRequireWithoutWithWildCard(): void
$this->validation->getError('a.1.c')
);
}

/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/8128
*/
public function testRuleWithAsteriskToMultiDimensionalArray(): void
{
$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
[
'name' => 'Fred Flinstone',
],
[
'name' => 'Wilma',
],
],
],
],
];

$this->validation->setRules(
['contacts.just.friends.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
[
'contacts.just.friends.0.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
'contacts.just.friends.1.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
],
$this->validation->getErrors()
);

$this->validation->reset();
$this->validation->setRules(
['contacts.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
// The data for `contacts.*.name` does not exist. So it is interpreted
// as `null`, and this error message returns.
['contacts.*.name' => 'The contacts.*.name field is required.'],
$this->validation->getErrors()
);
}
}
7 changes: 7 additions & 0 deletions user_guide_src/source/changelogs/v4.4.4.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,13 @@ Release Date: Unreleased
BREAKING
********

Validation with Dot Array Syntax
================================

A validation rule with the wildcard ``*`` now validates only data in correct
dimensions as "dot array syntax".
See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>` for details.

***************
Message Changes
***************
Expand Down
19 changes: 19 additions & 0 deletions user_guide_src/source/installation/upgrade_444.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,25 @@ Mandatory File Changes
Breaking Changes
****************

.. _upgrade-444-validation-with-dot-array-syntax:

Validation with Dot Array Syntax
================================

If you are using :ref:`dot array syntax <validation-dot-array-syntax>` in validation
rules, a bug where ``*`` would validate data in incorrect dimensions has been fixed.

In previous versions, the rule key ``contacts.*.name`` captured data with any
level like ``contacts.*.name``, ``contacts.*.*.name``, ``contacts.*.*.*.name``,
etc., incorrectly.

The following code explains details:

.. literalinclude:: upgrade_444/001.php
:lines: 2-

If you have code that depends on the bug, fix the the rule key.

*********************
Breaking Enhancements
*********************
Expand Down
38 changes: 38 additions & 0 deletions user_guide_src/source/installation/upgrade_444/001.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
<?php

use Config\Services;

$validation = Services::validation();

$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
['name' => 'SATO Taro'],
['name' => 'Li Ming'],
['name' => 'Heinz Müller'],
],
],
],
];

$validation->setRules(
['contacts.*.name' => 'required|max_length[8]']
);

$validation->run($data); // false

d($validation->getErrors());
/*
Before: Captured `contacts.*.*.*.name` incorrectly.
[
contacts.just.friends.0.name => "The contacts.*.name field cannot exceed 8 characters in length.",
contacts.just.friends.2.name => "The contacts.*.name field cannot exceed 8 characters in length.",
]

After: Captures no data for `contacts.*.name`.
[
contacts.*.name => string (38) "The contacts.*.name field is required.",
]
*/
15 changes: 11 additions & 4 deletions user_guide_src/source/libraries/validation.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -314,6 +314,8 @@ To give a labeled error message you can set up as:
.. note:: ``setRules()`` will overwrite any rules that were set previously. To add more than one
rule to an existing set of rules, use ``setRule()`` multiple times.

.. _validation-dot-array-syntax:

Setting Rules for Array Data
============================

Expand All@@ -328,6 +330,10 @@ You can use the ``*`` wildcard symbol to match any one level of the array:
.. literalinclude:: validation/010.php
:lines: 2-

.. note:: Prior to v4.4.4, due to a bug, the wildcard ``*`` validated data in incorrect
dimensions. See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>`
for details.

"dot array syntax" can also be useful when you have single dimension array data.
For example, data returned by multi select dropdown:

Expand DownExpand Up@@ -591,7 +597,7 @@ If you need to retrieve all error messages for failed fields, you can use the ``

If no errors exist, an empty array will be returned.

When using a wildcard, the error will point to a specific field, replacing the asterisk with the appropriate key/keys::
When using a wildcard (``*``), the error will point to a specific field, replacing the asterisk with the appropriate key/keys::

// for data
'contacts' => [
Expand All@@ -606,10 +612,10 @@ When using a wildcard, the error will point to a specific field, replacing the a
]

// rule
'contacts.*.name' => 'required'
'contacts.friends.*.name' => 'required'

// error will be
'contacts.friends.1.name' => 'The contacts.*.name field is required.'
'contacts.friends.1.name' => 'The contacts.friends.*.name field is required.'

Getting a Single Error
======================
Expand DownExpand Up@@ -830,7 +836,8 @@ alpha_numeric_punct No Fails if field contains anything other than
alphanumeric, space, or this limited set of
punctuation characters: ``~`` (tilde),
``!`` (exclamation), ``#`` (number),
``$`` (dollar), ``% (percent), & (ampersand),
``$`` (dollar), ``%`` (percent),
``&`` (ampersand),
``*`` (asterisk), ``-`` (dash),
``_`` (underscore), ``+`` (plus),
``=`` (equals), ``|`` (vertical bar),
Expand Down
7 changes: 1 addition & 6 deletions user_guide_src/source/libraries/validation/009.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@
* The data to test:
* [
* 'contacts' => [
* 'name' => 'Joe Smith',
* 'name' => 'Joe Smith',
* 'friends' => [
* [
* 'name' => 'Fred Flinstone',
Expand All@@ -21,8 +21,3 @@
$validation->setRules([
'contacts.name' => 'required|max_length[60]',
]);

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.friends.name' => 'required|max_length[60]',
]);
2 changes: 1 addition & 1 deletion user_guide_src/source/libraries/validation/010.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.*.name' => 'required|max_length[60]',
'contacts.friends.*.name' => 'required|max_length[60]',
]);
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions system/Validation/Validation.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -168,12 +168,14 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
}

if (strpos($field, '*') !== false) {
$values = array_filter(array_flatten_with_dots($data), static fn ($key) => preg_match(
'/^'
. str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/'))
. '$/',
$key
), ARRAY_FILTER_USE_KEY);
$flattenedArray = array_flatten_with_dots($data);

$values = array_filter(
$flattenedArray,
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

// if keys not found
$values = $values ?: [$field => null];
} else {
Expand DownExpand Up@@ -211,6 +213,20 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
return false;
}

/**
* Returns regex pattern for key with dot array syntax.
*/
private static function getRegex(string $field): string
{
return '/\A'
. str_replace(
['\.\*', '\*\.'],
['\.[^.]+', '[^.]+\.'],
preg_quote($field, '/')
)
. '\z/';
}

/**
* Runs the validation process, returning true or false determining whether
* validation was successful or not.
Expand DownExpand Up@@ -814,9 +830,7 @@ private function retrievePlaceholders(string $rule, array $data): array
*/
public function hasError(string $field): bool
{
$pattern = '/^' . str_replace('\.\*', '\..+', preg_quote($field, '/')) . '$/';

return (bool) preg_grep($pattern, array_keys($this->getErrors()));
return (bool) preg_grep(self::getRegex($field), array_keys($this->getErrors()));
}

/**
Expand All@@ -829,10 +843,11 @@ public function getError(?string $field = null): string
$field = array_key_first($this->rules);
}

$errors = array_filter($this->getErrors(), static fn ($key) => preg_match(
'/^' . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) . '$/',
$key
), ARRAY_FILTER_USE_KEY);
$errors = array_filter(
$this->getErrors(),
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

return $errors === [] ? '' : implode("\n", $errors);
}
Expand Down
66 changes: 56 additions & 10 deletions tests/system/Validation/ValidationTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,17 +1111,17 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$request = new IncomingRequest($config, new URI(), 'php://input', new UserAgent());

$this->validation->setRules([
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.*.name' => 'required',
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.friends.*.name' => 'required',
]);

$this->validation->withRequest($request->withMethod('post'))->run();
$this->assertSame([
'id_user.0' => 'The id_user.* field must contain only numbers.',
'name_user.0' => 'The name_user.* field may only contain alphabetical characters.',
'name_user.2' => 'The name_user.* field may only contain alphabetical characters.',
'contacts.friends.0.name' => 'The contacts.*.name field is required.',
'contacts.friends.0.name' => 'The contacts.friends.*.name field is required.',
], $this->validation->getErrors());

$this->assertSame(
Expand All@@ -1130,8 +1130,8 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$this->validation->getError('name_user.*')
);
$this->assertSame(
'The contacts.*.name field is required.',
$this->validation->getError('contacts.*.name')
'The contacts.friends.*.name field is required.',
$this->validation->getError('contacts.friends.*.name')
);
}

Expand DownExpand Up@@ -1228,17 +1228,17 @@ public function testTranslatedLabelTagReplacement(): void
}

/**
* @dataProvider provideDotNotationOnIfExistRule
* @dataProvider provideIfExistRuleWithAsterisk
*
* @see https://github.com/codeigniter4/CodeIgniter4/issues/4521
*/
public function testDotNotationOnIfExistRule(bool $expected, array $rules, array $data): void
public function testIfExistRuleWithAsterisk(bool $expected, array $rules, array $data): void
{
$actual = $this->validation->setRules($rules)->run($data);
$this->assertSame($expected, $actual);
}

public static function provideDotNotationOnIfExistRule(): iterable
public static function provideIfExistRuleWithAsterisk(): iterable
{
yield 'dot-on-end-fail' => [
false,
Expand DownExpand Up@@ -1613,7 +1613,7 @@ public function testRuleWithLeadingAsterisk(): void
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/5942
*/
public function testRequireWithoutWithWildCard(): void
public function testRequireWithoutWithAsterisk(): void
{
$data = [
'a' => [
Expand All@@ -1631,4 +1631,50 @@ public function testRequireWithoutWithWildCard(): void
$this->validation->getError('a.1.c')
);
}

/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/8128
*/
public function testRuleWithAsteriskToMultiDimensionalArray(): void
{
$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
[
'name' => 'Fred Flinstone',
],
[
'name' => 'Wilma',
],
],
],
],
];

$this->validation->setRules(
['contacts.just.friends.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
[
'contacts.just.friends.0.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
'contacts.just.friends.1.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
],
$this->validation->getErrors()
);

$this->validation->reset();
$this->validation->setRules(
['contacts.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
// The data for `contacts.*.name` does not exist. So it is interpreted
// as `null`, and this error message returns.
['contacts.*.name' => 'The contacts.*.name field is required.'],
$this->validation->getErrors()
);
}
}
7 changes: 7 additions & 0 deletions user_guide_src/source/changelogs/v4.4.4.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,13 @@ Release Date: Unreleased
BREAKING
********

Validation with Dot Array Syntax
================================

A validation rule with the wildcard ``*`` now validates only data in correct
dimensions as "dot array syntax".
See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>` for details.

***************
Message Changes
***************
Expand Down
19 changes: 19 additions & 0 deletions user_guide_src/source/installation/upgrade_444.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,25 @@ Mandatory File Changes
Breaking Changes
****************

.. _upgrade-444-validation-with-dot-array-syntax:

Validation with Dot Array Syntax
================================

If you are using :ref:`dot array syntax <validation-dot-array-syntax>` in validation
rules, a bug where ``*`` would validate data in incorrect dimensions has been fixed.

In previous versions, the rule key ``contacts.*.name`` captured data with any
level like ``contacts.*.name``, ``contacts.*.*.name``, ``contacts.*.*.*.name``,
etc., incorrectly.

The following code explains details:

.. literalinclude:: upgrade_444/001.php
:lines: 2-

If you have code that depends on the bug, fix the the rule key.

*********************
Breaking Enhancements
*********************
Expand Down
38 changes: 38 additions & 0 deletions user_guide_src/source/installation/upgrade_444/001.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
<?php

use Config\Services;

$validation = Services::validation();

$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
['name' => 'SATO Taro'],
['name' => 'Li Ming'],
['name' => 'Heinz Müller'],
],
],
],
];

$validation->setRules(
['contacts.*.name' => 'required|max_length[8]']
);

$validation->run($data); // false

d($validation->getErrors());
/*
Before: Captured `contacts.*.*.*.name` incorrectly.
[
contacts.just.friends.0.name => "The contacts.*.name field cannot exceed 8 characters in length.",
contacts.just.friends.2.name => "The contacts.*.name field cannot exceed 8 characters in length.",
]

After: Captures no data for `contacts.*.name`.
[
contacts.*.name => string (38) "The contacts.*.name field is required.",
]
*/
15 changes: 11 additions & 4 deletions user_guide_src/source/libraries/validation.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -314,6 +314,8 @@ To give a labeled error message you can set up as:
.. note:: ``setRules()`` will overwrite any rules that were set previously. To add more than one
rule to an existing set of rules, use ``setRule()`` multiple times.

.. _validation-dot-array-syntax:

Setting Rules for Array Data
============================

Expand All@@ -328,6 +330,10 @@ You can use the ``*`` wildcard symbol to match any one level of the array:
.. literalinclude:: validation/010.php
:lines: 2-

.. note:: Prior to v4.4.4, due to a bug, the wildcard ``*`` validated data in incorrect
dimensions. See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>`
for details.

"dot array syntax" can also be useful when you have single dimension array data.
For example, data returned by multi select dropdown:

Expand DownExpand Up@@ -591,7 +597,7 @@ If you need to retrieve all error messages for failed fields, you can use the ``

If no errors exist, an empty array will be returned.

When using a wildcard, the error will point to a specific field, replacing the asterisk with the appropriate key/keys::
When using a wildcard (``*``), the error will point to a specific field, replacing the asterisk with the appropriate key/keys::

// for data
'contacts' => [
Expand All@@ -606,10 +612,10 @@ When using a wildcard, the error will point to a specific field, replacing the a
]

// rule
'contacts.*.name' => 'required'
'contacts.friends.*.name' => 'required'

// error will be
'contacts.friends.1.name' => 'The contacts.*.name field is required.'
'contacts.friends.1.name' => 'The contacts.friends.*.name field is required.'

Getting a Single Error
======================
Expand DownExpand Up@@ -830,7 +836,8 @@ alpha_numeric_punct No Fails if field contains anything other than
alphanumeric, space, or this limited set of
punctuation characters: ``~`` (tilde),
``!`` (exclamation), ``#`` (number),
``$`` (dollar), ``% (percent), & (ampersand),
``$`` (dollar), ``%`` (percent),
``&`` (ampersand),
``*`` (asterisk), ``-`` (dash),
``_`` (underscore), ``+`` (plus),
``=`` (equals), ``|`` (vertical bar),
Expand Down
7 changes: 1 addition & 6 deletions user_guide_src/source/libraries/validation/009.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@
* The data to test:
* [
* 'contacts' => [
* 'name' => 'Joe Smith',
* 'name' => 'Joe Smith',
* 'friends' => [
* [
* 'name' => 'Fred Flinstone',
Expand All@@ -21,8 +21,3 @@
$validation->setRules([
'contacts.name' => 'required|max_length[60]',
]);

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.friends.name' => 'required|max_length[60]',
]);
2 changes: 1 addition & 1 deletion user_guide_src/source/libraries/validation/010.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.*.name' => 'required|max_length[60]',
'contacts.friends.*.name' => 'required|max_length[60]',
]);
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions system/Validation/Validation.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -168,12 +168,14 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
}

if (strpos($field, '*') !== false) {
$values = array_filter(array_flatten_with_dots($data), static fn ($key) => preg_match(
'/^'
. str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/'))
. '$/',
$key
), ARRAY_FILTER_USE_KEY);
$flattenedArray = array_flatten_with_dots($data);

$values = array_filter(
$flattenedArray,
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

// if keys not found
$values = $values ?: [$field => null];
} else {
Expand DownExpand Up@@ -211,6 +213,20 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
return false;
}

/**
* Returns regex pattern for key with dot array syntax.
*/
private static function getRegex(string $field): string
{
return '/\A'
. str_replace(
['\.\*', '\*\.'],
['\.[^.]+', '[^.]+\.'],
preg_quote($field, '/')
)
. '\z/';
}

/**
* Runs the validation process, returning true or false determining whether
* validation was successful or not.
Expand DownExpand Up@@ -814,9 +830,7 @@ private function retrievePlaceholders(string $rule, array $data): array
*/
public function hasError(string $field): bool
{
$pattern = '/^' . str_replace('\.\*', '\..+', preg_quote($field, '/')) . '$/';

return (bool) preg_grep($pattern, array_keys($this->getErrors()));
return (bool) preg_grep(self::getRegex($field), array_keys($this->getErrors()));
}

/**
Expand All@@ -829,10 +843,11 @@ public function getError(?string $field = null): string
$field = array_key_first($this->rules);
}

$errors = array_filter($this->getErrors(), static fn ($key) => preg_match(
'/^' . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) . '$/',
$key
), ARRAY_FILTER_USE_KEY);
$errors = array_filter(
$this->getErrors(),
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

return $errors === [] ? '' : implode("\n", $errors);
}
Expand Down
66 changes: 56 additions & 10 deletions tests/system/Validation/ValidationTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,17 +1111,17 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$request = new IncomingRequest($config, new URI(), 'php://input', new UserAgent());

$this->validation->setRules([
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.*.name' => 'required',
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.friends.*.name' => 'required',
]);

$this->validation->withRequest($request->withMethod('post'))->run();
$this->assertSame([
'id_user.0' => 'The id_user.* field must contain only numbers.',
'name_user.0' => 'The name_user.* field may only contain alphabetical characters.',
'name_user.2' => 'The name_user.* field may only contain alphabetical characters.',
'contacts.friends.0.name' => 'The contacts.*.name field is required.',
'contacts.friends.0.name' => 'The contacts.friends.*.name field is required.',
], $this->validation->getErrors());

$this->assertSame(
Expand All@@ -1130,8 +1130,8 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$this->validation->getError('name_user.*')
);
$this->assertSame(
'The contacts.*.name field is required.',
$this->validation->getError('contacts.*.name')
'The contacts.friends.*.name field is required.',
$this->validation->getError('contacts.friends.*.name')
);
}

Expand DownExpand Up@@ -1228,17 +1228,17 @@ public function testTranslatedLabelTagReplacement(): void
}

/**
* @dataProvider provideDotNotationOnIfExistRule
* @dataProvider provideIfExistRuleWithAsterisk
*
* @see https://github.com/codeigniter4/CodeIgniter4/issues/4521
*/
public function testDotNotationOnIfExistRule(bool $expected, array $rules, array $data): void
public function testIfExistRuleWithAsterisk(bool $expected, array $rules, array $data): void
{
$actual = $this->validation->setRules($rules)->run($data);
$this->assertSame($expected, $actual);
}

public static function provideDotNotationOnIfExistRule(): iterable
public static function provideIfExistRuleWithAsterisk(): iterable
{
yield 'dot-on-end-fail' => [
false,
Expand DownExpand Up@@ -1613,7 +1613,7 @@ public function testRuleWithLeadingAsterisk(): void
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/5942
*/
public function testRequireWithoutWithWildCard(): void
public function testRequireWithoutWithAsterisk(): void
{
$data = [
'a' => [
Expand All@@ -1631,4 +1631,50 @@ public function testRequireWithoutWithWildCard(): void
$this->validation->getError('a.1.c')
);
}

/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/8128
*/
public function testRuleWithAsteriskToMultiDimensionalArray(): void
{
$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
[
'name' => 'Fred Flinstone',
],
[
'name' => 'Wilma',
],
],
],
],
];

$this->validation->setRules(
['contacts.just.friends.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
[
'contacts.just.friends.0.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
'contacts.just.friends.1.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
],
$this->validation->getErrors()
);

$this->validation->reset();
$this->validation->setRules(
['contacts.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
// The data for `contacts.*.name` does not exist. So it is interpreted
// as `null`, and this error message returns.
['contacts.*.name' => 'The contacts.*.name field is required.'],
$this->validation->getErrors()
);
}
}
7 changes: 7 additions & 0 deletions user_guide_src/source/changelogs/v4.4.4.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,13 @@ Release Date: Unreleased
BREAKING
********

Validation with Dot Array Syntax
================================

A validation rule with the wildcard ``*`` now validates only data in correct
dimensions as "dot array syntax".
See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>` for details.

***************
Message Changes
***************
Expand Down
19 changes: 19 additions & 0 deletions user_guide_src/source/installation/upgrade_444.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,25 @@ Mandatory File Changes
Breaking Changes
****************

.. _upgrade-444-validation-with-dot-array-syntax:

Validation with Dot Array Syntax
================================

If you are using :ref:`dot array syntax <validation-dot-array-syntax>` in validation
rules, a bug where ``*`` would validate data in incorrect dimensions has been fixed.

In previous versions, the rule key ``contacts.*.name`` captured data with any
level like ``contacts.*.name``, ``contacts.*.*.name``, ``contacts.*.*.*.name``,
etc., incorrectly.

The following code explains details:

.. literalinclude:: upgrade_444/001.php
:lines: 2-

If you have code that depends on the bug, fix the the rule key.

*********************
Breaking Enhancements
*********************
Expand Down
38 changes: 38 additions & 0 deletions user_guide_src/source/installation/upgrade_444/001.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
<?php

use Config\Services;

$validation = Services::validation();

$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
['name' => 'SATO Taro'],
['name' => 'Li Ming'],
['name' => 'Heinz Müller'],
],
],
],
];

$validation->setRules(
['contacts.*.name' => 'required|max_length[8]']
);

$validation->run($data); // false

d($validation->getErrors());
/*
Before: Captured `contacts.*.*.*.name` incorrectly.
[
contacts.just.friends.0.name => "The contacts.*.name field cannot exceed 8 characters in length.",
contacts.just.friends.2.name => "The contacts.*.name field cannot exceed 8 characters in length.",
]

After: Captures no data for `contacts.*.name`.
[
contacts.*.name => string (38) "The contacts.*.name field is required.",
]
*/
15 changes: 11 additions & 4 deletions user_guide_src/source/libraries/validation.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -314,6 +314,8 @@ To give a labeled error message you can set up as:
.. note:: ``setRules()`` will overwrite any rules that were set previously. To add more than one
rule to an existing set of rules, use ``setRule()`` multiple times.

.. _validation-dot-array-syntax:

Setting Rules for Array Data
============================

Expand All@@ -328,6 +330,10 @@ You can use the ``*`` wildcard symbol to match any one level of the array:
.. literalinclude:: validation/010.php
:lines: 2-

.. note:: Prior to v4.4.4, due to a bug, the wildcard ``*`` validated data in incorrect
dimensions. See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>`
for details.

"dot array syntax" can also be useful when you have single dimension array data.
For example, data returned by multi select dropdown:

Expand DownExpand Up@@ -591,7 +597,7 @@ If you need to retrieve all error messages for failed fields, you can use the ``

If no errors exist, an empty array will be returned.

When using a wildcard, the error will point to a specific field, replacing the asterisk with the appropriate key/keys::
When using a wildcard (``*``), the error will point to a specific field, replacing the asterisk with the appropriate key/keys::

// for data
'contacts' => [
Expand All@@ -606,10 +612,10 @@ When using a wildcard, the error will point to a specific field, replacing the a
]

// rule
'contacts.*.name' => 'required'
'contacts.friends.*.name' => 'required'

// error will be
'contacts.friends.1.name' => 'The contacts.*.name field is required.'
'contacts.friends.1.name' => 'The contacts.friends.*.name field is required.'

Getting a Single Error
======================
Expand DownExpand Up@@ -830,7 +836,8 @@ alpha_numeric_punct No Fails if field contains anything other than
alphanumeric, space, or this limited set of
punctuation characters: ``~`` (tilde),
``!`` (exclamation), ``#`` (number),
``$`` (dollar), ``% (percent), & (ampersand),
``$`` (dollar), ``%`` (percent),
``&`` (ampersand),
``*`` (asterisk), ``-`` (dash),
``_`` (underscore), ``+`` (plus),
``=`` (equals), ``|`` (vertical bar),
Expand Down
7 changes: 1 addition & 6 deletions user_guide_src/source/libraries/validation/009.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@
* The data to test:
* [
* 'contacts' => [
* 'name' => 'Joe Smith',
* 'name' => 'Joe Smith',
* 'friends' => [
* [
* 'name' => 'Fred Flinstone',
Expand All@@ -21,8 +21,3 @@
$validation->setRules([
'contacts.name' => 'required|max_length[60]',
]);

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.friends.name' => 'required|max_length[60]',
]);
2 changes: 1 addition & 1 deletion user_guide_src/source/libraries/validation/010.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.*.name' => 'required|max_length[60]',
'contacts.friends.*.name' => 'required|max_length[60]',
]);
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions system/Validation/Validation.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -168,12 +168,14 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
}

if (strpos($field, '*') !== false) {
$values = array_filter(array_flatten_with_dots($data), static fn ($key) => preg_match(
'/^'
. str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/'))
. '$/',
$key
), ARRAY_FILTER_USE_KEY);
$flattenedArray = array_flatten_with_dots($data);

$values = array_filter(
$flattenedArray,
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

// if keys not found
$values = $values ?: [$field => null];
} else {
Expand DownExpand Up@@ -211,6 +213,20 @@ public function run(?array $data = null, ?string $group = null, ?string $dbGroup
return false;
}

/**
* Returns regex pattern for key with dot array syntax.
*/
private static function getRegex(string $field): string
{
return '/\A'
. str_replace(
['\.\*', '\*\.'],
['\.[^.]+', '[^.]+\.'],
preg_quote($field, '/')
)
. '\z/';
}

/**
* Runs the validation process, returning true or false determining whether
* validation was successful or not.
Expand DownExpand Up@@ -814,9 +830,7 @@ private function retrievePlaceholders(string $rule, array $data): array
*/
public function hasError(string $field): bool
{
$pattern = '/^' . str_replace('\.\*', '\..+', preg_quote($field, '/')) . '$/';

return (bool) preg_grep($pattern, array_keys($this->getErrors()));
return (bool) preg_grep(self::getRegex($field), array_keys($this->getErrors()));
}

/**
Expand All@@ -829,10 +843,11 @@ public function getError(?string $field = null): string
$field = array_key_first($this->rules);
}

$errors = array_filter($this->getErrors(), static fn ($key) => preg_match(
'/^' . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) . '$/',
$key
), ARRAY_FILTER_USE_KEY);
$errors = array_filter(
$this->getErrors(),
static fn ($key) => preg_match(self::getRegex($field), $key),
ARRAY_FILTER_USE_KEY
);

return $errors === [] ? '' : implode("\n", $errors);
}
Expand Down
66 changes: 56 additions & 10 deletions tests/system/Validation/ValidationTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -1111,17 +1111,17 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$request = new IncomingRequest($config, new URI(), 'php://input', new UserAgent());

$this->validation->setRules([
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.*.name' => 'required',
'id_user.*' => 'numeric',
'name_user.*' => 'alpha',
'contacts.friends.*.name' => 'required',
]);

$this->validation->withRequest($request->withMethod('post'))->run();
$this->assertSame([
'id_user.0' => 'The id_user.* field must contain only numbers.',
'name_user.0' => 'The name_user.* field may only contain alphabetical characters.',
'name_user.2' => 'The name_user.* field may only contain alphabetical characters.',
'contacts.friends.0.name' => 'The contacts.*.name field is required.',
'contacts.friends.0.name' => 'The contacts.friends.*.name field is required.',
], $this->validation->getErrors());

$this->assertSame(
Expand All@@ -1130,8 +1130,8 @@ public function testRulesForSingleRuleWithAsteriskWillReturnError(): void
$this->validation->getError('name_user.*')
);
$this->assertSame(
'The contacts.*.name field is required.',
$this->validation->getError('contacts.*.name')
'The contacts.friends.*.name field is required.',
$this->validation->getError('contacts.friends.*.name')
);
}

Expand DownExpand Up@@ -1228,17 +1228,17 @@ public function testTranslatedLabelTagReplacement(): void
}

/**
* @dataProvider provideDotNotationOnIfExistRule
* @dataProvider provideIfExistRuleWithAsterisk
*
* @see https://github.com/codeigniter4/CodeIgniter4/issues/4521
*/
public function testDotNotationOnIfExistRule(bool $expected, array $rules, array $data): void
public function testIfExistRuleWithAsterisk(bool $expected, array $rules, array $data): void
{
$actual = $this->validation->setRules($rules)->run($data);
$this->assertSame($expected, $actual);
}

public static function provideDotNotationOnIfExistRule(): iterable
public static function provideIfExistRuleWithAsterisk(): iterable
{
yield 'dot-on-end-fail' => [
false,
Expand DownExpand Up@@ -1613,7 +1613,7 @@ public function testRuleWithLeadingAsterisk(): void
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/5942
*/
public function testRequireWithoutWithWildCard(): void
public function testRequireWithoutWithAsterisk(): void
{
$data = [
'a' => [
Expand All@@ -1631,4 +1631,50 @@ public function testRequireWithoutWithWildCard(): void
$this->validation->getError('a.1.c')
);
}

/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/8128
*/
public function testRuleWithAsteriskToMultiDimensionalArray(): void
{
$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
[
'name' => 'Fred Flinstone',
],
[
'name' => 'Wilma',
],
],
],
],
];

$this->validation->setRules(
['contacts.just.friends.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
[
'contacts.just.friends.0.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
'contacts.just.friends.1.name' => 'The contacts.just.friends.*.name field cannot exceed 1 characters in length.',
],
$this->validation->getErrors()
);

$this->validation->reset();
$this->validation->setRules(
['contacts.*.name' => 'required|max_length[1]']
);
$this->assertFalse($this->validation->run($data));
$this->assertSame(
// The data for `contacts.*.name` does not exist. So it is interpreted
// as `null`, and this error message returns.
['contacts.*.name' => 'The contacts.*.name field is required.'],
$this->validation->getErrors()
);
}
}
7 changes: 7 additions & 0 deletions user_guide_src/source/changelogs/v4.4.4.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,13 @@ Release Date: Unreleased
BREAKING
********

Validation with Dot Array Syntax
================================

A validation rule with the wildcard ``*`` now validates only data in correct
dimensions as "dot array syntax".
See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>` for details.

***************
Message Changes
***************
Expand Down
19 changes: 19 additions & 0 deletions user_guide_src/source/installation/upgrade_444.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,25 @@ Mandatory File Changes
Breaking Changes
****************

.. _upgrade-444-validation-with-dot-array-syntax:

Validation with Dot Array Syntax
================================

If you are using :ref:`dot array syntax <validation-dot-array-syntax>` in validation
rules, a bug where ``*`` would validate data in incorrect dimensions has been fixed.

In previous versions, the rule key ``contacts.*.name`` captured data with any
level like ``contacts.*.name``, ``contacts.*.*.name``, ``contacts.*.*.*.name``,
etc., incorrectly.

The following code explains details:

.. literalinclude:: upgrade_444/001.php
:lines: 2-

If you have code that depends on the bug, fix the the rule key.

*********************
Breaking Enhancements
*********************
Expand Down
38 changes: 38 additions & 0 deletions user_guide_src/source/installation/upgrade_444/001.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
<?php

use Config\Services;

$validation = Services::validation();

$data = [
'contacts' => [
'name' => 'Joe Smith',
'just' => [
'friends' => [
['name' => 'SATO Taro'],
['name' => 'Li Ming'],
['name' => 'Heinz Müller'],
],
],
],
];

$validation->setRules(
['contacts.*.name' => 'required|max_length[8]']
);

$validation->run($data); // false

d($validation->getErrors());
/*
Before: Captured `contacts.*.*.*.name` incorrectly.
[
contacts.just.friends.0.name => "The contacts.*.name field cannot exceed 8 characters in length.",
contacts.just.friends.2.name => "The contacts.*.name field cannot exceed 8 characters in length.",
]

After: Captures no data for `contacts.*.name`.
[
contacts.*.name => string (38) "The contacts.*.name field is required.",
]
*/
15 changes: 11 additions & 4 deletions user_guide_src/source/libraries/validation.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -314,6 +314,8 @@ To give a labeled error message you can set up as:
.. note:: ``setRules()`` will overwrite any rules that were set previously. To add more than one
rule to an existing set of rules, use ``setRule()`` multiple times.

.. _validation-dot-array-syntax:

Setting Rules for Array Data
============================

Expand All@@ -328,6 +330,10 @@ You can use the ``*`` wildcard symbol to match any one level of the array:
.. literalinclude:: validation/010.php
:lines: 2-

.. note:: Prior to v4.4.4, due to a bug, the wildcard ``*`` validated data in incorrect
dimensions. See :ref:`Upgrading <upgrade-444-validation-with-dot-array-syntax>`
for details.

"dot array syntax" can also be useful when you have single dimension array data.
For example, data returned by multi select dropdown:

Expand DownExpand Up@@ -591,7 +597,7 @@ If you need to retrieve all error messages for failed fields, you can use the ``

If no errors exist, an empty array will be returned.

When using a wildcard, the error will point to a specific field, replacing the asterisk with the appropriate key/keys::
When using a wildcard (``*``), the error will point to a specific field, replacing the asterisk with the appropriate key/keys::

// for data
'contacts' => [
Expand All@@ -606,10 +612,10 @@ When using a wildcard, the error will point to a specific field, replacing the a
]

// rule
'contacts.*.name' => 'required'
'contacts.friends.*.name' => 'required'

// error will be
'contacts.friends.1.name' => 'The contacts.*.name field is required.'
'contacts.friends.1.name' => 'The contacts.friends.*.name field is required.'

Getting a Single Error
======================
Expand DownExpand Up@@ -830,7 +836,8 @@ alpha_numeric_punct No Fails if field contains anything other than
alphanumeric, space, or this limited set of
punctuation characters: ``~`` (tilde),
``!`` (exclamation), ``#`` (number),
``$`` (dollar), ``% (percent), & (ampersand),
``$`` (dollar), ``%`` (percent),
``&`` (ampersand),
``*`` (asterisk), ``-`` (dash),
``_`` (underscore), ``+`` (plus),
``=`` (equals), ``|`` (vertical bar),
Expand Down
7 changes: 1 addition & 6 deletions user_guide_src/source/libraries/validation/009.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@
* The data to test:
* [
* 'contacts' => [
* 'name' => 'Joe Smith',
* 'name' => 'Joe Smith',
* 'friends' => [
* [
* 'name' => 'Fred Flinstone',
Expand All@@ -21,8 +21,3 @@
$validation->setRules([
'contacts.name' => 'required|max_length[60]',
]);

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.friends.name' => 'required|max_length[60]',
]);
2 changes: 1 addition & 1 deletion user_guide_src/source/libraries/validation/010.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,5 +2,5 @@

// Fred Flintsone & Wilma
$validation->setRules([
'contacts.*.name' => 'required|max_length[60]',
'contacts.friends.*.name' => 'required|max_length[60]',
]);