Commit 38e0df1
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 experimentalFile tree
27 files changed
+2163
-1
lines changed- src/Symfony/Component
- JsonPath
- .github
- workflows
- Exception
- Tests
- Tokenizer
- Tokenizer
- JsonStreamer/Read
27 files changed
+2163
-1
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
| 86 | + | |
86 | 87 | | |
87 | 88 | | |
88 | 89 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
Lines changed: 21 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
Lines changed: 21 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
Lines changed: 25 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
Lines changed: 27 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
0 commit comments