diff --git a/src/Symfony/Component/JsonPath/JsonCrawler.php b/src/Symfony/Component/JsonPath/JsonCrawler.php index 492f56e77bba7..d5fe0af6d70dc 100644 --- a/src/Symfony/Component/JsonPath/JsonCrawler.php +++ b/src/Symfony/Component/JsonPath/JsonCrawler.php @@ -228,7 +228,56 @@ private function evaluateBracket(string $expr, mixed $value): array return $this->evaluateFilter($innerExpr, $value); } - // quoted strings for object keys + // comma-separated values, e.g. `['key1', 'key2', 123]` or `[0, 1, 'key']` + if (str_contains($expr, ',')) { + $parts = $this->parseCommaSeparatedValues($expr); + + $result = []; + $keysIndices = array_keys($value); + $isList = array_is_list($value); + + foreach ($parts as $part) { + $part = trim($part); + + if (preg_match('/^([\'"])(.*)\1$/', $part, $matches)) { + $key = JsonPathUtils::unescapeString($matches[2], $matches[1]); + + if ($isList) { + foreach ($value as $item) { + if (\is_array($item) && \array_key_exists($key, $item)) { + $result[] = $item; + break; + } + } + + continue; // no results here + } + + if (\array_key_exists($key, $value)) { + $result[] = $value[$key]; + } + } elseif (preg_match('/^-?\d+$/', $part)) { + // numeric index + $index = (int) $part; + if ($index < 0) { + $index = \count($value) + $index; + } + + if ($isList && \array_key_exists($index, $value)) { + $result[] = $value[$index]; + continue; + } + + // numeric index on a hashmap + if (isset($keysIndices[$index]) && isset($value[$keysIndices[$index]])) { + $result[] = $value[$keysIndices[$index]]; + } + } + } + + return $result; + } + if (preg_match('/^([\'"])(.*)\1$/', $expr, $matches)) { $key = JsonPathUtils::unescapeString($matches[2], $matches[1]); @@ -415,4 +464,44 @@ private function compare(mixed $left, mixed $right, string $operator): bool default => false, }; } + + private function parseCommaSeparatedValues(string $expr): array + { + $parts = []; + $current = ''; + $inQuotes = false; + $quoteChar = null; + + for ($i = 0; $i < \strlen($expr); ++$i) { + $char = $expr[$i]; + + if ('\\' === $char && $i + 1 < \strlen($expr)) { + $current .= $char.$expr[++$i]; + continue; + } + + if ('"' === $char || "'" === $char) { + if (!$inQuotes) { + $inQuotes = true; + $quoteChar = $char; + } elseif ($char === $quoteChar) { + $inQuotes = false; + $quoteChar = null; + } + } elseif (!$inQuotes && ',' === $char) { + $parts[] = trim($current); + $current = ''; + + continue; + } + + $current .= $char; + } + + if ('' !== $current) { + $parts[] = trim($current); + } + + return $parts; + } } diff --git a/src/Symfony/Component/JsonPath/Tests/JsonCrawlerTest.php b/src/Symfony/Component/JsonPath/Tests/JsonCrawlerTest.php index 213ae06afa7db..7f07f829bb901 100644 --- a/src/Symfony/Component/JsonPath/Tests/JsonCrawlerTest.php +++ b/src/Symfony/Component/JsonPath/Tests/JsonCrawlerTest.php @@ -91,6 +91,35 @@ public function testEscapedDoubleQuotesInFieldName() $this->assertSame(42, $result[0]); } + public function testMultipleKeysAtOnce() + { + $crawler = new JsonCrawler(<<find("$['a', 'b', 3]"); + + $this->assertSame([ + ['b"c' => 42], + ['c' => 43], + ], $result); + } + + public function testMultipleKeysAtOnceOnArray() + { + $crawler = new JsonCrawler(<<find("$[0, 2, 'a,b,c', -1]"); + + $this->assertCount(4, $result); + $this->assertSame(['a' => 1], $result[0]); + $this->assertSame(['c' => 3], $result[1]); + $this->assertSame(['a,b,c' => 5], $result[2]); + $this->assertSame(['d' => 4], $result[3]); + } + public function testBasicNameSelector() { $result = self::getBookstoreCrawler()->find('$.store.book')[0];