Skip to content

Commit e9eb3e7

Browse files
bug #60668 [JsonPath] Always use brackets notation with JsonPath::key() (alexandre-daubois)
This PR was merged into the 7.3 branch. Discussion ---------- [JsonPath] Always use brackets notation with `JsonPath::key()` | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | Fix #60664 | License | MIT As explained in the issue, the dot notation can lead to invalid paths when used with some identifiers (for example, when it contains a `-`). The issue gives the following example: ```php use Symfony\Component\JsonPath\JsonCrawler; use Symfony\Component\JsonPath\JsonPath; $jsonPathExpression = (string) (new JsonPath)->key('some-prop'); dump((new JsonCrawler('{"some-prop": "example value"}'))->find($jsonPathExpression)); ``` Let's always use the brackets notation in `JsonPath::key()`. Dot notation is only a shorthand to the verbose brackets version and this solves the problem without having to do further analysis on the key. Added a few tests to ensure the brackets notation works well too. Commits ------- 442970b [JsonPath] Always use brackets notation with `JsonPath::key()`
2 parents 0577b23 + 442970b commit e9eb3e7

File tree

3 files changed

+133
-7
lines changed

3 files changed

+133
-7
lines changed

src/Symfony/Component/JsonPath/JsonPath.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ public function __construct(
3030

3131
public function key(string $key): static
3232
{
33-
return new self($this->path.(str_ends_with($this->path, '..') ? '' : '.').$key);
33+
$escaped = $this->escapeKey($key);
34+
35+
return new self($this->path.'["'.$escaped.'"]');
3436
}
3537

3638
public function index(int $index): static
@@ -80,4 +82,25 @@ public function __toString(): string
8082
{
8183
return $this->path;
8284
}
85+
86+
private function escapeKey(string $key): string
87+
{
88+
$key = strtr($key, [
89+
'\\' => '\\\\',
90+
'"' => '\\"',
91+
"\n" => '\\n',
92+
"\r" => '\\r',
93+
"\t" => '\\t',
94+
"\b" => '\\b',
95+
"\f" => '\\f'
96+
]);
97+
98+
for ($i = 0; $i <= 31; $i++) {
99+
if ($i < 8 || $i > 13) {
100+
$key = str_replace(chr($i), sprintf('\\u%04x', $i), $key);
101+
}
102+
}
103+
104+
return $key;
105+
}
83106
}

src/Symfony/Component/JsonPath/Tests/JsonCrawlerTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@ public function testAllAuthors()
4949
], $result);
5050
}
5151

52+
public function testAllAuthorsWithBrackets()
53+
{
54+
$result = self::getBookstoreCrawler()->find('$..["author"]');
55+
56+
$this->assertCount(4, $result);
57+
$this->assertSame([
58+
'Nigel Rees',
59+
'Evelyn Waugh',
60+
'Herman Melville',
61+
'J. R. R. Tolkien',
62+
], $result);
63+
}
64+
5265
public function testAllThingsInStore()
5366
{
5467
$result = self::getBookstoreCrawler()->find('$.store.*');
@@ -58,6 +71,15 @@ public function testAllThingsInStore()
5871
$this->assertArrayHasKey('color', $result[1]);
5972
}
6073

74+
public function testAllThingsInStoreWithBrackets()
75+
{
76+
$result = self::getBookstoreCrawler()->find('$["store"][*]');
77+
78+
$this->assertCount(2, $result);
79+
$this->assertCount(4, $result[0]);
80+
$this->assertArrayHasKey('color', $result[1]);
81+
}
82+
6183
public function testEscapedDoubleQuotesInFieldName()
6284
{
6385
$crawler = new JsonCrawler(<<<JSON
@@ -77,6 +99,14 @@ public function testBasicNameSelector()
7799
$this->assertSame('Nigel Rees', $result[0]['author']);
78100
}
79101

102+
public function testBasicNameSelectorWithBrackts()
103+
{
104+
$result = self::getBookstoreCrawler()->find('$["store"]["book"]')[0];
105+
106+
$this->assertCount(4, $result);
107+
$this->assertSame('Nigel Rees', $result[0]['author']);
108+
}
109+
80110
public function testAllPrices()
81111
{
82112
$result = self::getBookstoreCrawler()->find('$.store..price');
@@ -121,6 +151,17 @@ public function testBooksWithIsbn()
121151
], [$result[0]['isbn'], $result[1]['isbn']]);
122152
}
123153

154+
public function testBooksWithBracketsAndFilter()
155+
{
156+
$result = self::getBookstoreCrawler()->find('$..["book"][?(@.isbn)]');
157+
158+
$this->assertCount(2, $result);
159+
$this->assertSame([
160+
'0-553-21311-3',
161+
'0-395-19395-8',
162+
], [$result[0]['isbn'], $result[1]['isbn']]);
163+
}
164+
124165
public function testBooksLessThanTenDollars()
125166
{
126167
$result = self::getBookstoreCrawler()->find('$..book[?(@.price < 10)]');
@@ -216,6 +257,14 @@ public function testEverySecondElementReverseSlice()
216257
$this->assertSame([6, 2, 5], $result);
217258
}
218259

260+
public function testEverySecondElementReverseSliceAndBrackets()
261+
{
262+
$crawler = self::getSimpleCollectionCrawler();
263+
264+
$result = $crawler->find('$["a"][::-2]');
265+
$this->assertSame([6, 2, 5], $result);
266+
}
267+
219268
public function testEmptyResults()
220269
{
221270
$crawler = self::getSimpleCollectionCrawler();
@@ -404,6 +453,19 @@ public function testAcceptsJsonPath()
404453
$this->assertSame('red', $result[0]['color']);
405454
}
406455

456+
public function testStarAsKey()
457+
{
458+
$crawler = new JsonCrawler(<<<JSON
459+
{"*": {"a": 1, "b": 2}, "something else": {"c": 3}}
460+
JSON);
461+
462+
$result = $crawler->find('$["*"]');
463+
464+
$this->assertCount(1, $result);
465+
$this->assertSame(['a' => 1, 'b' => 2], $result[0]);
466+
}
467+
468+
407469
private static function getBookstoreCrawler(): JsonCrawler
408470
{
409471
return new JsonCrawler(<<<JSON

src/Symfony/Component/JsonPath/Tests/JsonPathTest.php

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ public function testBuildPath()
2323
->index(0)
2424
->key('address');
2525

26-
$this->assertSame('$.users[0].address', (string) $path);
27-
$this->assertSame('$.users[0].address..city', (string) $path->deepScan()->key('city'));
26+
$this->assertSame('$["users"][0]["address"]', (string) $path);
27+
$this->assertSame('$["users"][0]["address"]..["city"]', (string) $path->deepScan()->key('city'));
2828
}
2929

3030
public function testBuildWithFilter()
@@ -33,7 +33,7 @@ public function testBuildWithFilter()
3333
$path = $path->key('users')
3434
->filter('@.age > 18');
3535

36-
$this->assertSame('$.users[?(@.age > 18)]', (string) $path);
36+
$this->assertSame('$["users"][?(@.age > 18)]', (string) $path);
3737
}
3838

3939
public function testAll()
@@ -42,7 +42,7 @@ public function testAll()
4242
$path = $path->key('users')
4343
->all();
4444

45-
$this->assertSame('$.users[*]', (string) $path);
45+
$this->assertSame('$["users"][*]', (string) $path);
4646
}
4747

4848
public function testFirst()
@@ -51,7 +51,7 @@ public function testFirst()
5151
$path = $path->key('users')
5252
->first();
5353

54-
$this->assertSame('$.users[0]', (string) $path);
54+
$this->assertSame('$["users"][0]', (string) $path);
5555
}
5656

5757
public function testLast()
@@ -60,6 +60,47 @@ public function testLast()
6060
$path = $path->key('users')
6161
->last();
6262

63-
$this->assertSame('$.users[-1]', (string) $path);
63+
$this->assertSame('$["users"][-1]', (string) $path);
64+
}
65+
66+
/**
67+
* @dataProvider provideKeysToEscape
68+
*/
69+
public function testEscapedKey(string $key, string $expectedPath)
70+
{
71+
$path = new JsonPath();
72+
$path = $path->key($key);
73+
74+
$this->assertSame($expectedPath, (string) $path);
75+
}
76+
77+
public static function provideKeysToEscape(): iterable
78+
{
79+
yield ['simple_key', '$["simple_key"]'];
80+
yield ['key"with"quotes', '$["key\\"with\\"quotes"]'];
81+
yield ['path\\backslash', '$["path\\backslash"]'];
82+
yield ['mixed\\"case', '$["mixed\\\\\\"case"]'];
83+
yield ['unicode_🔑', '$["unicode_🔑"]'];
84+
yield ['"quotes_only"', '$["\\"quotes_only\\""]'];
85+
yield ['\\\\multiple\\\\backslashes', '$["\\\\\\\\multiple\\\\\\backslashes"]'];
86+
yield ["control\x00\x1f\x1echar", '$["control\u0000\u001f\u001echar"]'];
87+
88+
yield ['key"with\\"mixed', '$["key\\"with\\\\\\"mixed"]'];
89+
yield ['\\"complex\\"case\\"', '$["\\\\\\"complex\\\\\\"case\\\\\\""]'];
90+
yield ['json_like":{"value":"test"}', '$["json_like\\":{\\"value\\":\\"test\\"}"]'];
91+
yield ['C:\\Program Files\\"App Name"', '$["C:\\\\Program Files\\\\\\"App Name\\""]'];
92+
93+
yield ['key_with_é_accents', '$["key_with_é_accents"]'];
94+
yield ['unicode_→_arrows', '$["unicode_→_arrows"]'];
95+
yield ['chinese_中文_key', '$["chinese_中文_key"]'];
96+
97+
yield ['', '$[""]'];
98+
yield [' ', '$[" "]'];
99+
yield [' spaces ', '$[" spaces "]'];
100+
yield ["\t\n\r", '$["\\t\\n\\r"]'];
101+
yield ["control\x00char", '$["control\u0000char"]'];
102+
yield ["newline\nkey", '$["newline\\nkey"]'];
103+
yield ["tab\tkey", '$["tab\\tkey"]'];
104+
yield ["carriage\rreturn", '$["carriage\\rreturn"]'];
64105
}
65106
}

0 commit comments

Comments
 (0)