Skip to content

Commit a6e3913

Browse files
bug #60504 [JsonPath] Fix subexpression evaluation in filters (alexandre-daubois)
This PR was merged into the 7.3 branch. Discussion ---------- [JsonPath] Fix subexpression evaluation in filters | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | - | License | MIT This PR fixes complex expressions in filters evaluation like `$.store.book[?(@.publisher.address.city == "Springfield")]` or `$.store.book[?match(@.publisher.*.city, "Spring.+")]`. It also brings better support to the `@` operator when used alone. Commits ------- eb289c7fc88 [JsonPath] Fix subexpression evaluation in filters
2 parents 8d6feb7 + f222907 commit a6e3913

File tree

2 files changed

+95
-26
lines changed

2 files changed

+95
-26
lines changed

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) {
@@ -295,10 +300,6 @@ private function evaluateFilter(string $expr, mixed $value): array
295300

296301
$result = [];
297302
foreach ($value as $item) {
298-
if (!\is_array($item)) {
299-
continue;
300-
}
301-
302303
if ($this->evaluateFilterExpression($expr, $item)) {
303304
$result[] = $item;
304305
}
@@ -307,7 +308,7 @@ private function evaluateFilter(string $expr, mixed $value): array
307308
return $result;
308309
}
309310

310-
private function evaluateFilterExpression(string $expr, array $context): bool
311+
private function evaluateFilterExpression(string $expr, mixed $context): bool
311312
{
312313
$expr = trim($expr);
313314

@@ -343,10 +344,12 @@ private function evaluateFilterExpression(string $expr, array $context): bool
343344
}
344345
}
345346

346-
if (str_starts_with($expr, '@.')) {
347-
$path = substr($expr, 2);
347+
if ('@' === $expr) {
348+
return true;
349+
}
348350

349-
return \array_key_exists($path, $context);
351+
if (str_starts_with($expr, '@.')) {
352+
return (bool) ($this->evaluateTokensOnDecodedData(JsonPathTokenizer::tokenize(new JsonPath('$'.substr($expr, 1))), $context)[0] ?? false);
350353
}
351354

352355
// function calls
@@ -364,12 +367,16 @@ private function evaluateFilterExpression(string $expr, array $context): bool
364367
return false;
365368
}
366369

367-
private function evaluateScalar(string $expr, array $context): mixed
370+
private function evaluateScalar(string $expr, mixed $context): mixed
368371
{
369372
if (is_numeric($expr)) {
370373
return str_contains($expr, '.') ? (float) $expr : (int) $expr;
371374
}
372375

376+
if ('@' === $expr) {
377+
return $context;
378+
}
379+
373380
if ('true' === $expr) {
374381
return true;
375382
}
@@ -388,10 +395,12 @@ private function evaluateScalar(string $expr, array $context): mixed
388395
}
389396

390397
// current node references
391-
if (str_starts_with($expr, '@.')) {
392-
$path = substr($expr, 2);
398+
if (str_starts_with($expr, '@')) {
399+
if (!\is_array($context)) {
400+
return null;
401+
}
393402

394-
return $context[$path] ?? null;
403+
return $this->evaluateTokensOnDecodedData(JsonPathTokenizer::tokenize(new JsonPath('$'.substr($expr, 1))), $context)[0] ?? null;
395404
}
396405

397406
// function calls

Tests/JsonCrawlerTest.php

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

183+
public function testBooksWithPublisherAddress()
184+
{
185+
$result = self::getBookstoreCrawler()->find('$..book[?(@.publisher.address)]');
186+
187+
$this->assertCount(1, $result);
188+
$this->assertSame('Sword of Honour', $result[0]['title']);
189+
}
190+
183191
public function testBooksWithBracketsAndFilter()
184192
{
185193
$result = self::getBookstoreCrawler()->find('$..["book"][?(@.isbn)]');
@@ -422,6 +430,50 @@ public function testValueFunction()
422430
$this->assertSame('Sayings of the Century', $result[0]['title']);
423431
}
424432

433+
public function testDeepExpressionInFilter()
434+
{
435+
$result = self::getBookstoreCrawler()->find('$.store.book[?(@.publisher.address.city == "Springfield")]');
436+
437+
$this->assertCount(1, $result);
438+
$this->assertSame('Sword of Honour', $result[0]['title']);
439+
}
440+
441+
public function testWildcardInFilter()
442+
{
443+
$result = self::getBookstoreCrawler()->find('$.store.book[?(@.publisher.* == "my-publisher")]');
444+
445+
$this->assertCount(1, $result);
446+
$this->assertSame('Sword of Honour', $result[0]['title']);
447+
}
448+
449+
public function testWildcardInFunction()
450+
{
451+
$result = self::getBookstoreCrawler()->find('$.store.book[?match(@.publisher.*.city, "Spring.+")]');
452+
453+
$this->assertCount(1, $result);
454+
$this->assertSame('Sword of Honour', $result[0]['title']);
455+
}
456+
457+
public function testUseAtSymbolReturnsAll()
458+
{
459+
$result = self::getBookstoreCrawler()->find('$.store.bicycle[?(@ == @)]');
460+
461+
$this->assertSame([
462+
'red',
463+
399,
464+
], $result);
465+
}
466+
467+
public function testUseAtSymbolAloneReturnsAll()
468+
{
469+
$result = self::getBookstoreCrawler()->find('$.store.bicycle[?(@)]');
470+
471+
$this->assertSame([
472+
'red',
473+
399,
474+
], $result);
475+
}
476+
425477
public function testValueFunctionWithOuterParentheses()
426478
{
427479
$result = self::getBookstoreCrawler()->find('$.store.book[?(value(@.price) == 8.95)]');
@@ -756,7 +808,15 @@ private static function getBookstoreCrawler(): JsonCrawler
756808
"category": "fiction",
757809
"author": "Evelyn Waugh",
758810
"title": "Sword of Honour",
759-
"price": 12.99
811+
"price": 12.99,
812+
"publisher": {
813+
"name": "my-publisher",
814+
"address": {
815+
"street": "1234 Elm St",
816+
"city": "Springfield",
817+
"state": "IL"
818+
}
819+
}
760820
},
761821
{
762822
"category": "fiction",

0 commit comments

Comments
 (0)