Skip to content

[JsonPath] Fix subexpression evaluation in filters #60504

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 7.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 34 additions & 25 deletions src/Symfony/Component/JsonPath/JsonCrawler.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,26 +80,31 @@ private function evaluate(JsonPath $query): array
throw new InvalidJsonStringInputException($e->getMessage(), $e);
}

$current = [$data];

foreach ($tokens as $token) {
$next = [];
foreach ($current as $value) {
$result = $this->evaluateToken($token, $value);
$next = array_merge($next, $result);
}

$current = $next;
}

return $current;
return $this->evaluateTokensOnDecodedData($tokens, $data);
} catch (InvalidArgumentException $e) {
throw $e;
} catch (\Throwable $e) {
throw new JsonCrawlerException($query, $e->getMessage(), previous: $e);
}
}

private function evaluateTokensOnDecodedData(array $tokens, array $data): array
{
$current = [$data];

foreach ($tokens as $token) {
$next = [];
foreach ($current as $value) {
$result = $this->evaluateToken($token, $value);
$next = array_merge($next, $result);
}

$current = $next;
}

return $current;
}

private function evaluateToken(JsonPathToken $token, mixed $value): array
{
return match ($token->type) {
Expand Down Expand Up @@ -246,10 +251,6 @@ private function evaluateFilter(string $expr, mixed $value): array

$result = [];
foreach ($value as $item) {
if (!\is_array($item)) {
continue;
}

if ($this->evaluateFilterExpression($expr, $item)) {
$result[] = $item;
}
Expand All @@ -258,7 +259,7 @@ private function evaluateFilter(string $expr, mixed $value): array
return $result;
}

private function evaluateFilterExpression(string $expr, array $context): bool
private function evaluateFilterExpression(string $expr, mixed $context): bool
{
$expr = trim($expr);

Expand Down Expand Up @@ -294,10 +295,12 @@ private function evaluateFilterExpression(string $expr, array $context): bool
}
}

if (str_starts_with($expr, '@.')) {
$path = substr($expr, 2);
if ('@' === $expr) {
return true;
}

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

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

private function evaluateScalar(string $expr, array $context): mixed
private function evaluateScalar(string $expr, mixed $context): mixed
{
if (is_numeric($expr)) {
return str_contains($expr, '.') ? (float) $expr : (int) $expr;
}

if ('@' === $expr) {
return $context;
}

if ('true' === $expr) {
return true;
}
Expand All @@ -339,10 +346,12 @@ private function evaluateScalar(string $expr, array $context): mixed
}

// current node references
if (str_starts_with($expr, '@.')) {
$path = substr($expr, 2);
if (str_starts_with($expr, '@')) {
if (!\is_array($context)) {
return null;
}

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

// function calls
Expand Down
62 changes: 61 additions & 1 deletion src/Symfony/Component/JsonPath/Tests/JsonCrawlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ public function testBooksWithIsbn()
], [$result[0]['isbn'], $result[1]['isbn']]);
}

public function testBooksWithPublisherAddress()
{
$result = self::getBookstoreCrawler()->find('$..book[?(@.publisher.address)]');

$this->assertCount(1, $result);
$this->assertSame('Sword of Honour', $result[0]['title']);
}

public function testBooksLessThanTenDollars()
{
$result = self::getBookstoreCrawler()->find('$..book[?(@.price < 10)]');
Expand Down Expand Up @@ -344,6 +352,50 @@ public function testValueFunction()
$this->assertSame('Sayings of the Century', $result[0]['title']);
}

public function testDeepExpressionInFilter()
{
$result = self::getBookstoreCrawler()->find('$.store.book[?(@.publisher.address.city == "Springfield")]');

$this->assertCount(1, $result);
$this->assertSame('Sword of Honour', $result[0]['title']);
}

public function testWildcardInFilter()
{
$result = self::getBookstoreCrawler()->find('$.store.book[?(@.publisher.* == "my-publisher")]');

$this->assertCount(1, $result);
$this->assertSame('Sword of Honour', $result[0]['title']);
}

public function testWildcardInFunction()
{
$result = self::getBookstoreCrawler()->find('$.store.book[?match(@.publisher.*.city, "Spring.+")]');

$this->assertCount(1, $result);
$this->assertSame('Sword of Honour', $result[0]['title']);
}

public function testUseAtSymbolReturnsAll()
{
$result = self::getBookstoreCrawler()->find('$.store.bicycle[?(@ == @)]');

$this->assertSame([
'red',
399,
], $result);
}

public function testUseAtSymbolAloneReturnsAll()
{
$result = self::getBookstoreCrawler()->find('$.store.bicycle[?(@)]');

$this->assertSame([
'red',
399,
], $result);
}

public function testValueFunctionWithOuterParentheses()
{
$result = self::getBookstoreCrawler()->find('$.store.book[?(value(@.price) == 8.95)]');
Expand Down Expand Up @@ -420,7 +472,15 @@ private static function getBookstoreCrawler(): JsonCrawler
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
"price": 12.99,
"publisher": {
"name": "my-publisher",
"address": {
"street": "1234 Elm St",
"city": "Springfield",
"state": "IL"
}
}
},
{
"category": "fiction",
Expand Down
Loading