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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy