Skip to content

Commit def3a69

Browse files
authored
Fix #610: Add $escape parameter to methods Result::getAttributeErrorMessagesIndexedByPath() and Result::getErrorMessagesIndexedByPath() that allow change or disable symbol which will be escaped in value path elements, fix #612: Disable escaping of asterisk char in value path returned by Error::getValuePath(true)
1 parent ebf6636 commit def3a69

9 files changed

Lines changed: 168 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
## 1.2.0 under development
44

55
- New #597, #608: Add debug collector for `yiisoft/yii-debug` (@xepozz, @vjik)
6+
- New #610: Add `$escape` parameter to methods `Result::getAttributeErrorMessagesIndexedByPath()` and
7+
`Result::getErrorMessagesIndexedByPath()` that allow change or disable symbol which will be escaped in value path
8+
elements (@vjik)
9+
- Bug #612: Disable escaping of asterisk char in value path returned by `Error::getValuePath(true)` (@vjik)
610

711
## 1.1.0 April 06, 2023
812

docs/guide/en/result.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,12 @@ A path can contain integer elements too (when using the `Each` rule for example)
178178

179179
#### Resolving special characters collision in attribute names
180180

181-
When the attribute name contains a path separator (dot - `.` by default) or `Each` rule shortcut (asterisk -`*`),
182-
they're automatically escaped using a backslash (`\`) in the error messages list:
181+
When the attribute name in the error messages list contains a path separator (dot `.` by default),
182+
it is automatically escaped using a backslash (`\`):
183183

184184
```php
185185
[
186-
'\*country\.code' => ['Value cannot be blank.'],
186+
'country\.code' => ['Value cannot be blank.'],
187187
],
188188
```
189189

@@ -195,7 +195,7 @@ use Yiisoft\Validator\Rule\In;
195195
use Yiisoft\Validator\Rule\Required;
196196

197197
$rules = [
198-
'*country.code' => [
198+
'country.code' => [
199199
new Required();
200200
new In(['ru', 'en'], skipOnError: true),
201201
],

src/AfterInitAttributeEventInterface.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace Yiisoft\Validator;
66

7-
use Attribute;
87
use Yiisoft\Validator\RulesProvider\AttributesRulesProvider;
98

109
/**

src/Error.php

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Yiisoft\Validator;
66

7+
use InvalidArgumentException;
78
use Yiisoft\Translator\IntlMessageFormatter;
89
use Yiisoft\Translator\SimpleMessageFormatter;
910
use Yiisoft\Validator\Rule\Callback;
@@ -94,21 +95,32 @@ public function getParameters(): array
9495
* A getter for {@see $valuePath} property. Returns a sequence of keys determining where a value caused the
9596
* validation error is located within a nested structure.
9697
*
97-
* @param bool $escape Whether to escape a dot (`'.'`, used as a separator in a string representation) and asterisk
98-
* (`'*``, used as a {@see Each} rule shortcut) char with a backslash char (`'\'`).
98+
* @param bool|string|null $escape Symbol that will be escaped with a backslash char (`\`) in path elements.
99+
* When it's null path is returned without escaping.
100+
* Boolean value is deprecated and will be removed in the next major release. Boolean value processed in the following way:
101+
* - `false` as null,
102+
* - `true` as dot (`.`).
99103
*
100104
* @return array A list of keys for nested structures or an empty array otherwise.
101105
*
102106
* @psalm-return list<int|string>
103107
*/
104-
public function getValuePath(bool $escape = false): array
108+
public function getValuePath(bool|string|null $escape = false): array
105109
{
106-
if ($escape === false) {
110+
if ($escape === false || $escape === null) {
107111
return $this->valuePath;
108112
}
109113

114+
if ($escape === true) {
115+
$escape = '.';
116+
}
117+
118+
if (mb_strlen($escape) !== 1) {
119+
throw new InvalidArgumentException('Escape symbol must be exactly one character.');
120+
}
121+
110122
return array_map(
111-
static fn ($key): string => str_replace(['.', '*'], ['\\' . '.', '\\' . '*'], (string) $key),
123+
static fn($key): string => str_replace($escape, '\\' . $escape, (string) $key),
112124
$this->valuePath,
113125
);
114126
}

src/Result.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,18 @@ public function getErrorMessages(): array
7373
* Each value is an array of error message strings.
7474
*
7575
* @param string $separator Attribute path separator. Dot is used by default.
76+
* @param string|null $escape Symbol that will be escaped with a backslash char (`\`) in path elements.
77+
* When it's null path is returned without escaping.
7678
*
7779
* @return array Arrays of error messages indexed by attribute path.
7880
*
7981
* @psalm-return array<string, non-empty-list<string>>
8082
*/
81-
public function getErrorMessagesIndexedByPath(string $separator = '.'): array
83+
public function getErrorMessagesIndexedByPath(string $separator = '.', ?string $escape = '.'): array
8284
{
8385
$errors = [];
8486
foreach ($this->errors as $error) {
85-
$stringValuePath = implode($separator, $error->getValuePath(true));
87+
$stringValuePath = implode($separator, $error->getValuePath($escape));
8688
$errors[$stringValuePath][] = $error->getMessage();
8789
}
8890

@@ -158,21 +160,26 @@ public function getAttributeErrorMessages(string $attribute): array
158160
*
159161
* @param string $attribute Attribute name.
160162
* @param string $separator Attribute path separator. Dot is used by default.
163+
* @param string|null $escape Symbol that will be escaped with a backslash char (`\`) in path elements.
164+
* When it's null path is returned without escaping.
161165
*
162166
* @return array Arrays of error messages for the attribute specified indexed by attribute path.
163167
*
164168
* @psalm-return array<string, non-empty-list<string>>
165169
*/
166-
public function getAttributeErrorMessagesIndexedByPath(string $attribute, string $separator = '.'): array
167-
{
170+
public function getAttributeErrorMessagesIndexedByPath(
171+
string $attribute,
172+
string $separator = '.',
173+
?string $escape = '.',
174+
): array {
168175
$errors = [];
169176
foreach ($this->errors as $error) {
170177
$firstItem = $error->getValuePath()[0] ?? '';
171178
if ($firstItem !== $attribute) {
172179
continue;
173180
}
174181

175-
$valuePath = implode($separator, array_slice($error->getValuePath(true), 1));
182+
$valuePath = implode($separator, array_slice($error->getValuePath($escape), 1));
176183
$errors[$valuePath][] = $error->getMessage();
177184
}
178185

src/Rule/Nested.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ private function handleEachShortcut(array &$rules): void
499499
$breakWhile = false;
500500

501501
$lastValuePath = array_pop($parts);
502-
$lastValuePath = ltrim($lastValuePath, '.');
502+
$lastValuePath = ltrim($lastValuePath, self::SEPARATOR);
503503
$lastValuePath = str_replace('\\' . self::EACH_SHORTCUT, self::EACH_SHORTCUT, $lastValuePath);
504504

505505
$remainingValuePath = implode(self::EACH_SHORTCUT, $parts);

tests/ErrorTest.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Validator\Tests;
6+
7+
use InvalidArgumentException;
8+
use PHPUnit\Framework\TestCase;
9+
use Yiisoft\Validator\Error;
10+
11+
final class ErrorTest extends TestCase
12+
{
13+
public function dataGetValuePath(): array
14+
{
15+
return [
16+
'null' => [
17+
['user', 'data.age'],
18+
['user', 'data.age'],
19+
null,
20+
],
21+
'symbol' => [
22+
['user', 'the.data\-age'],
23+
['user', 'the.data-age'],
24+
'-',
25+
],
26+
'emoji' => [
27+
['user', 'the.data\😎age'],
28+
['user', 'the.data😎age'],
29+
'😎',
30+
],
31+
32+
// deprecated
33+
'true' => [
34+
['user', 'data\.age'],
35+
['user', 'data.age'],
36+
true,
37+
],
38+
39+
// deprecated
40+
'false' => [
41+
['user', 'data.age'],
42+
['user', 'data.age'],
43+
false,
44+
],
45+
];
46+
}
47+
48+
/**
49+
* @dataProvider dataGetValuePath
50+
*/
51+
public function testGetValuePath(array $expectedValuePath, array $valuePath, bool|string|null $escape): void
52+
{
53+
$error = new Error('', valuePath: $valuePath);
54+
55+
$this->assertSame($expectedValuePath, $error->getValuePath($escape));
56+
}
57+
58+
public function testTooLongEscapeSymbol(): void
59+
{
60+
$error = new Error('');
61+
62+
$this->expectException(InvalidArgumentException::class);
63+
$this->expectExceptionMessage('Escape symbol must be exactly one character.');
64+
$error->getValuePath('..');
65+
}
66+
}

tests/ResultTest.php

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
use PHPUnit\Framework\TestCase;
99
use Yiisoft\Validator\Error;
1010
use Yiisoft\Validator\Result;
11+
use Yiisoft\Validator\Rule\Callback;
12+
use Yiisoft\Validator\Rule\Nested;
13+
use Yiisoft\Validator\Validator;
1114

1215
class ResultTest extends TestCase
1316
{
@@ -68,8 +71,8 @@ public function testGetErrorMessagesIndexedByPathWithAttributes(): void
6871
'attribute2' => ['error2.1', 'error2.2'],
6972
'attribute2.nested' => ['error2.3', 'error2.4'],
7073
'' => ['error3.1', 'error3.2'],
71-
'attribute4.subattribute4\.1.subattribute4\*2' => ['error4.1'],
72-
'attribute4.subattribute4\.3.subattribute4\*4' => ['error4.2'],
74+
'attribute4.subattribute4\.1.subattribute4*2' => ['error4.1'],
75+
'attribute4.subattribute4\.3.subattribute4*4' => ['error4.2'],
7376
],
7477
$this->createAttributeErrorResult()->getErrorMessagesIndexedByPath()
7578
);
@@ -164,8 +167,8 @@ public function testGetAttributeErrorMessagesIndexedByPath(): void
164167
);
165168
$this->assertEquals(['' => ['error3.1', 'error3.2']], $result->getAttributeErrorMessagesIndexedByPath(''));
166169
$this->assertEquals([
167-
'subattribute4\.1.subattribute4\*2' => ['error4.1'],
168-
'subattribute4\.3.subattribute4\*4' => ['error4.2'],
170+
'subattribute4\.1.subattribute4*2' => ['error4.1'],
171+
'subattribute4\.3.subattribute4*4' => ['error4.2'],
169172
], $result->getAttributeErrorMessagesIndexedByPath('attribute4'));
170173
}
171174

@@ -174,6 +177,59 @@ public function testGetCommonErrorMessages(): void
174177
$this->assertEquals(['error3.1', 'error3.2'], $this->createAttributeErrorResult()->getCommonErrorMessages());
175178
}
176179

180+
/**
181+
* @see https://github.com/yiisoft/validator/issues/610
182+
*/
183+
public function testDataKeysWithDots(): void
184+
{
185+
$result = (new Validator())->validate(
186+
[
187+
'user.age' => 17,
188+
'meta' => [
189+
'tag' => 'hi',
190+
],
191+
],
192+
[
193+
'user.age' => static fn() => (new Result())->addError('Too young.'),
194+
'meta' => new Nested([
195+
'tag' => new Callback(static fn() => (new Result())->addError('Too short.')),
196+
]),
197+
],
198+
);
199+
200+
$this->assertSame(
201+
[
202+
'user\.age' => ['Too young.'],
203+
'meta.tag' => ['Too short.'],
204+
],
205+
$result->getErrorMessagesIndexedByPath()
206+
);
207+
}
208+
209+
public function testEscapeInGetErrorMessagesIndexedByPath(): void
210+
{
211+
$result = (new Result())->addError('e1', valuePath: ['user', 'meta.the-age']);
212+
213+
$this->assertSame(
214+
[
215+
'user.meta.the\-age' => ['e1'],
216+
],
217+
$result->getErrorMessagesIndexedByPath(escape: '-'),
218+
);
219+
}
220+
221+
public function testEscapeInGetAttributeErrorMessagesIndexedByPath(): void
222+
{
223+
$result = (new Result())->addError('e1', valuePath: ['user', 'data', 'meta.the-age']);
224+
225+
$this->assertSame(
226+
[
227+
'data.meta.the\-age' => ['e1'],
228+
],
229+
$result->getAttributeErrorMessagesIndexedByPath('user', escape: '-'),
230+
);
231+
}
232+
177233
private function createAttributeErrorResult(): Result
178234
{
179235
$result = new Result();

tests/Rule/NestedTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -875,10 +875,10 @@ public function dataWithOtherNestedAndEach(): array
875875
],
876876
array_slice($errorMessages, 0, 5),
877877
[
878-
'charts\.list.0.points\*list.0.coordinates\.data.x' => [$errorMessages[0], $errorMessages[1]],
879-
'charts\.list.0.points\*list.0.coordinates\.data.y' => [$errorMessages[2]],
880-
'charts\.list.0.points\*list.0.rgb.0' => [$errorMessages[3]],
881-
'charts\.list.0.points\*list.0.rgb.1' => [$errorMessages[4]],
878+
'charts\.list.0.points*list.0.coordinates\.data.x' => [$errorMessages[0], $errorMessages[1]],
879+
'charts\.list.0.points*list.0.coordinates\.data.y' => [$errorMessages[2]],
880+
'charts\.list.0.points*list.0.rgb.0' => [$errorMessages[3]],
881+
'charts\.list.0.points*list.0.rgb.1' => [$errorMessages[4]],
882882
],
883883
],
884884
];

0 commit comments

Comments
 (0)