8000 feature #59655 [JsonPath] Add the component (alexandre-daubois) · symfony/symfony@38e0df1 · GitHub
[go: up one dir, main page]

Skip to content

Commit 38e0df1

Browse files
committed
feature #59655 [JsonPath] Add the component (alexandre-daubois)
This PR was merged into the 7.3 branch. Discussion ---------- [JsonPath] Add the component | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | Fix #57280 | License | MIT ## JsonPath component Today I'm presenting the JsonPath component. Thanks to this component, it will be possible to query JSON strings using the JSON Path syntax, as described in the recent RFC 9535. This RFC was released in February 2024 and is published here: https://datatracker.ietf.org/doc/html/rfc9535. Here's a preview of what's possible: ```php <?php require 'vendor/autoload.php'; use Symfony\Component\JsonPath\JsonPath; use Symfony\Component\JsonPath\JsonCrawler; $json = <<<'JSON' {"store": {"book": [ {"category": "reference", "author": "Nigel Rees", "title": "Sayings", "price": 8.95}, {"category": "fiction", "author": "Evelyn Waugh", "title": "Sword", "price": 12.99} ]}} JSON; $crawler = new JsonCrawler($json); // Basic property access $result = $crawler->find('$.store.book[0].title'); // Array slice $result = $crawler->find('$.store.book[0:2]'); // Reverse array slice $result = $crawler->find('$.store.book[::-1]'); // Filter expression $result = $crawler->find('$.store.book[?(@.price < 10)]'); // Recursive descent $result = $crawler->find('$..author'); // Call to "length()" $result = $crawler->find('$.store.book[?length(@.author) > 11]'); // Call to "match()" $result = $crawler->find('$.store.book[?match(@.author, "[A-Z].*el.+")]'); // use a builder to create your path $path = new JsonPath(); $path = $path->key('book') ->index(0) ->key('author'); $result = $crawler->find($path); ``` As stated in RFC 9535, this component embeds a few read-to-use functions: - `length` - `count` - `value` - `search` - `match` ### Integration of JsonStreamer for performance Thanks to the powerfulness of JsonStreamer's Splitter, we're able to guess which part of the JSON needs and doesn't need to be decoded. Querying a whole node means we can only `json_decode()` the substring containing the node. The same goes when encountering array keys. We extract the relevant substring thanks to Splitter that provides an API for that. It brings support for JSON-as-resource evaluation. We stop when we encounter an operation that implies filtering as we need to decode the whole node to filter children. ### What about other packages? A few alternatives exist, however they were not updated in the last few years. If they are up-to-date, they do not seem to be following RFC 9535, but rather a partial implementation of https://goessner.net/articles/JsonPath/. ### Why not include it in DomCrawler or PropertyAccess? That was my first thought, however DomCrawler and JsonPath actually share absolutely no logic and their purpose is really different. As they have not much in common, that would be no ideal to tie them up. PropertyAccess could be another possibility, here's where I explained why I think this would not be a correct fit: #59655 (comment). ### Does it need external dependencies? No! This component is written in vanilla PHP and doesn't require any third-party package to work. ### How will it be leveraged in Symfony/PHP? So many possibilities 😉 The first I can think of is leveraging this component in integration tests. Indeed, this would allow to easily validate and write new assert methods when writing integration tests with Symfony. Validating JSON returned by an API thanks to this notation would be way easier (and more readable). In short, HttpClient, BrowserKit and `WebTestCase` could beneficiate from this (especially `BrowserKitAssertionsTrait` where asserts on Json could be added!). Symfony would not be the only beneficiary: we can easily imagine that libraries like Behat (or PHPUnit, why not) could use this package to implement asserts on JSON. Apart from testing frameworks, this package can also be used to quickly extract precise data from a JSON. So, possibilities here are also endless. ### Possible evolution - Leverage ExpressionLanguage to evaluate filters and allow user to inject its own filter functions (custom functions are allowed per the RFC) - Improve the JsonPath builder - XPath may be converted to JSON Path fairly easily, this could be something to investigate on Commits ------- f34f4c4 [JsonPath] Add the component as experimental
2 parents 82cf90a + f34f4c4 commit 38e0df1

27 files changed

+2163
-1
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"symfony/http-foundation": "self.version",
8484
"symfony/http-kernel": "self.version",
8585
"symfony/intl": "self.version",
86+
"symfony/json-path": "self.version",
8687
"symfony/json-streamer": "self.version",
8788
"symfony/ldap": "self.version",
8889
"symfony/lock": "self.version",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.git* export-ignore

src/Symfony/Component/JsonPath/.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Symfony/Component/JsonPath/.github/workflows/close-pull-request.yml

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
7.3
5+
---
6+
7+
* Add the component as experimental
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\JsonPath\Exception;
13+
14+
/**
15+
* @author Alexandre Daubois <alex.daubois@gmail.com>
16+
*
17+
* 23D3 @experimental
18+
*/
19+
interface ExceptionInterface extends \Throwable
20+
{
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\JsonPath\Exception;
13+
14+
/**
15+
* @author Alexandre Daubois <alex.daubois@gmail.com>
16+
*
17+
* @experimental
18+
*/
19+
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
20+
{
21+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\JsonPath\Exception;
13+
14+
/**
15+
* @author Alexandre Daubois <alex.daubois@gmail.com>
16+
*
17+
* @experimental
18+
*/
19+
class InvalidJsonPathException extends \LogicException implements ExceptionInterface
20+
{
21+
public function __construct(string $message, ?int $position = null)
22+
{
23+
parent::__construct(\sprintf('JSONPath syntax error%s: %s', $position ? ' at position '.$position : '', $message));
24+
}
25+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\JsonPath\Exception;
13+
14+
/**
15+
* Thrown when a string passed as an input is not a valid JSON string, e.g. in {@see JsonCrawler}.
16+
*
17+
* @author Alexandre Daubois <alex.daubois@gmail.com>
18+
*
19+
* @experimental
20+
*/
21+
class InvalidJsonStringInputException extends InvalidArgumentException
22+
{
23+
public function __construct(string $message, ?\Throwable $previous = null)
24+
{
25+
parent::__construct(\sprintf('Invalid JSON input: %s.', $message), previous: $previous);
26+
}
27+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\JsonPath\Exception;
13+
14+
/**
15+
* @author Alexandre Daubois <alex.daubois@gmail.com>
16+
*
17+
* @experimental
18+
*/
19+
class JsonCrawlerException extends \RuntimeException implements ExceptionInterface
20+
{
21+
public function __construct(string $path, string $message, ?\Throwable $previous = null)
22+
{
23+
parent::__construct(\sprintf('Error while crawling JSON with JSON path "%s": %s.', $path, $message), previous: $previous);
24+
}
25+
}

0 commit comments

Comments
 (0)
0