Skip to content

Commit ac09c66

Browse files
[JsonPath] Fix subexpression evaluation in filters
1 parent cb08480 commit ac09c66

File tree

2 files changed

+95
-26
lines changed

2 files changed

+95
-26
lines changed

src/Symfony/Component/JsonPath/JsonCrawler.php

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -80,26 +80,31 @@ private function evaluate(JsonPath $query): array
8080
throw new InvalidJsonStringInputException($e->getMessage(), $e);
8181
}
8282

83-
$current = [$data];
84-
85-
foreach ($tokens as $token) {
86-
$next = [];
87-
foreach ($current as $value) {
88-
$result = $this->evaluateToken($token, $value);
89-
$next = array_merge($next, $result);
90-
}
91-
92-
$current = $next;
93-
}
94-
95-
return $current;
83+
return $this->evaluateTokensOnDecodedData($tokens, $data);
9684
} catch (InvalidArgumentException $e) {
9785
throw $e;
9886
} catch (\Throwable $e) {
9987
throw new JsonCrawlerException($query, $e->getMessage(), previous: $e);
10088
}
10189
}
10290

91+
private function evaluateTokensOnDecodedData(array $tokens, array $data): array
92+
{
93+
$current = [$data];
94+
95+
foreach ($tokens as $token) {
96+
$next = [];
97+
foreach ($current as $value) {
98+
$result = $this->evaluateToken($token, $value);
99+
$next = array_merge($next, $result);
100+
}
101+
102+
$current = $next;
103+
}
104+
105+
return $current;
106+
}
107+
103108
private function evaluateToken(JsonPathToken $token, mixed $value): array
104109
{
105110
return match ($token->type) {
@@ -246,10 +251,6 @@ private function evaluateFilter(string $expr, mixed $value): array
246251

247252
$result = [];
248253
foreach ($value as $item) {
249-
if (!\is_array($item)) {
250-
continue;
251-
}
252-
253254
if ($this->evaluateFilterExpression($expr, $item)) {
254255
$result[] = $item;
255256
}
@@ -258,7 +259,7 @@ private function evaluateFilter(string $expr, mixed $value): array
258259
return $result;
259260
}
260261

261-
private function evaluateFilterExpression(string $expr, array $context): bool
262+
private function evaluateFilterExpression(string $expr, mixed $context): bool
262263
{
263264
$expr = trim($expr);
264265

@@ -294,10 +295,12 @@ private function evaluateFilterExpression(string $expr, array $context): bool
294295
}
295296
}
296297

297-
if (str_starts_with($expr, '@.')) {
298-
$path = substr($expr, 2);
298+
if ('@' === $expr) {
299+
return true;
300+
}
299301

300-
return \array_key_exists($path, $context);
302+
if (str_starts_with($expr, '@.')) {
303+
return (bool) ($this->evaluateTokensOnDecodedData(JsonPathTokenizer::tokenize(new JsonPath('$'. substr($expr, 1))), $context)[0] ?? false);
301304
}
302305

303306
// function calls
@@ -315,12 +318,16 @@ private function evaluateFilterExpression(string $expr, array $context): bool
315318
return false;
316319
}
317320

318-
private function evaluateScalar(string $expr, array $context): mixed
321+
private function evaluateScalar(string $expr, mixed $context): mixed
319322
{
320323
if (is_numeric($expr)) {
321324
return str_contains($expr, '.') ? (float) $expr : (int) $expr;
322325
}
323326

327+
if ('@' === $expr) {
328+
return $context;
329+
}
330+
324331
if ('true' === $expr) {
325332
return true;
326333
}
@@ -339,10 +346,12 @@ private function evaluateScalar(string $expr, array $context): mixed
339346
}
340347

341348
// current node references
342-
if (str_starts_with($expr, '@.')) {
343-
$path = substr($expr, 2);
349+
if (str_starts_with($expr, '@')) {
350+
if (!\is_array($context)) {
351+
return null;
352+
}
344353

345-
return $context[$path] ?? null;
354+
return $this->evaluateTokensOnDecodedData(JsonPathTokenizer::tokenize(new JsonPath('$'. substr($expr, 1))), $context)[0] ?? null;
346355
}
347356

348357
// function calls

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

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,14 @@ public function testBooksWithIsbn()
121121
], [$result[0]['isbn'], $result[1]['isbn']]);
122122
}
123123

124+
public function testBooksWithPublisherAddress()
125+
{
126+
$result = self::getBookstoreCrawler()->find('$..book[?(@.publisher.address)]');
127+
128+
$this->assertCount(1, $result);
129+
$this->assertSame('Sword of Honour', $result[0]['title']);
130+
}
131+
124132
public function testBooksLessThanTenDollars()
125133
{
126134
$result = self::getBookstoreCrawler()->find('$..book[?(@.price < 10)]');
@@ -344,6 +352,50 @@ public function testValueFunction()
344352
$this->assertSame('Sayings of the Century', $result[0]['title']);
345353
}
346354

355+
public function testDeepExpressionInFilter()
356+
{
357+
$result = self::getBookstoreCrawler()->find('$.store.book[?(@.publisher.address.city == "Springfield")]');
358+
359+
$this->assertCount(1, $result);
360+
$this->assertSame('Sword of Honour', $result[0]['title']);
361+
}
362+
363+
public function testWildcardInFilter()
364+
{
365+
$result = self::getBookstoreCrawler()->find('$.store.book[?(@.publisher.* == "my-publisher")]');
366+
367+
$this->assertCount(1, $result);
368+
$this->assertSame('Sword of Honour', $result[0]['title']);
369+
}
370+
371+
public function testWildcardInFunction()
372+
{
373+
$result = self::getBookstoreCrawler()->find('$.store.book[?match(@.publisher.*.city, "Spring.+")]');
374+
375+
$this->assertCount(1, $result);
376+
$this->assertSame('Sword of Honour', $result[0]['title']);
377+
}
378+
379+
public function testUseAtSymbolReturnsAll()
380+
{
381+
$result = self::getBookstoreCrawler()->find('$.store.bicycle[?(@ == @)]');
382+
383+
$this->assertSame([
384+
'red',
385+
399,
386+
], $result);
387+
}
388+
389+
public function testUseAtSymbolAloneReturnsAll()
390+
{
391+
$result = self::getBookstoreCrawler()->find('$.store.bicycle[?(@)]');
392+
393+
$this->assertSame([
394+
'red',
395+
399,
396+
], $result);
397+
}
398+
347399
public function testValueFunctionWithOuterParentheses()
348400
{
349401
$result = self::getBookstoreCrawler()->find('$.store.book[?(value(@.price) == 8.95)]');
@@ -420,7 +472,15 @@ private static function getBookstoreCrawler(): JsonCrawler
420472
"category": "fiction",
421473
"author": "Evelyn Waugh",
422474
"title": "Sword of Honour",
423-
"price": 12.99
475+
"price": 12.99,
476+
"publisher": {
477+
"name": "my-publisher",
478+
"address": {
479+
"street": "1234 Elm St",
480+
"city": "Springfield",
481+
"state": "IL"
482+
}
483+
}
424484
},
425485
{
426486
"category": "fiction",

0 commit comments

Comments
 (0)