├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── composer.json
├── phpunit.xml
├── src
└── Flow
│ └── JSONPath
│ ├── AccessHelper.php
│ ├── Filters
│ ├── AbstractFilter.php
│ ├── IndexFilter.php
│ ├── IndexesFilter.php
│ ├── QueryMatchFilter.php
│ ├── QueryResultFilter.php
│ ├── RecursiveFilter.php
│ └── SliceFilter.php
│ ├── JSONPath.php
│ ├── JSONPathException.php
│ ├── JSONPathLexer.php
│ └── JSONPathToken.php
└── tests
├── JSONPathArrayAccessTest.php
├── JSONPathDashedIndexTest.php
├── JSONPathLexerTest.php
├── JSONPathSliceAccessTest.php
└── JSONPathTest.php
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | composer.lock
3 |
4 | .idea
5 |
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | php:
3 | - '7.1'
4 | - '7.2'
5 | - '7.3'
6 | - 'nightly'
7 |
8 |
9 | install:
10 | - composer install
11 |
12 | script:
13 | - vendor/bin/phpunit -c phpunit.xml
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Flow Communications
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # :exclamation: New project maintainers :exclamation:
2 |
3 | This project is no longer maintained here. Please go to https://github.com/SoftCreatR/JSONPath.
4 |
5 | JSONPath [](https://travis-ci.org/FlowCommunications/JSONPath)
6 | =============
7 |
8 | This is a [JSONPath](http://goessner.net/articles/JsonPath/) implementation for PHP based on Stefan Goessner's JSONPath script.
9 |
10 | JSONPath is an XPath-like expression language for filtering, flattening and extracting data.
11 |
12 | This project aims to be a clean and simple implementation with the following goals:
13 |
14 | - Object-oriented code (should be easier to manage or extend in future)
15 | - Expressions are parsed into tokens using code inspired by the Doctrine Lexer. The tokens are cached internally to avoid re-parsing the expressions.
16 | - There is no `eval()` in use
17 | - Any combination of objects/arrays/ArrayAccess-objects can be used as the data input which is great if you're de-serializing JSON in to objects
18 | or if you want to process your own data structures.
19 |
20 | Installation
21 | ---
22 |
23 | **PHP 7.1+**
24 | ```bash
25 | composer require flow/jsonpath
26 | ```
27 | **PHP 5.4 - 5.6**
28 |
29 | Support for PHP 5.x is deprecated... the current version should work but all unit tests are run against 7.1+ and support may be dropped at any time in the future.
30 |
31 | A legacy branch is maintained in `php-5.x` and can be composer-installed as follows:
32 | `"flow/jsonpath": "dev-php-5.x"`
33 |
34 | JSONPath Examples
35 | ---
36 |
37 | JSONPath | Result
38 | --------------------------|-------------------------------------
39 | `$.store.books[*].author` | the authors of all books in the store
40 | `$..author` | all authors
41 | `$.store..price` | the price of everything in the store.
42 | `$..books[2]` | the third book
43 | `$..books[(@.length-1)]` | the last book in order.
44 | `$..books[-1:]` | the last book in order.
45 | `$..books[0,1]` | the first two books
46 | `$..books[:2]` | the first two books
47 | `$..books[::2]` | every second book starting from first one
48 | `$..books[1:6:3]` | every third book starting from 1 till 6
49 | `$..books[?(@.isbn)]` | filter all books with isbn number
50 | `$..books[?(@.price<10)]` | filter all books cheapier than 10
51 | `$..*` | all elements in the data (recursively extracted)
52 |
53 |
54 | Expression syntax
55 | ---
56 |
57 | Symbol | Description
58 | ----------------------|-------------------------
59 | `$` | The root object/element (not strictly necessary)
60 | `@` | The current object/element
61 | `.` or `[]` | Child operator
62 | `..` | Recursive descent
63 | `*` | Wildcard. All child elements regardless their index.
64 | `[,]` | Array indices as a set
65 | `[start:end:step]` | Array slice operator borrowed from ES4/Python.
66 | `?()` | Filters a result set by a script expression
67 | `()` | Uses the result of a script expression as the index
68 |
69 | PHP Usage
70 | ---
71 |
72 | ```php
73 | $data = ['people' => [['name' => 'Joe'], ['name' => 'Jane'], ['name' => 'John']]];
74 | $result = (new JSONPath($data))->find('$.people.*.name'); // returns new JSONPath
75 | // $result[0] === 'Joe'
76 | // $result[1] === 'Jane'
77 | // $result[2] === 'John'
78 | ```
79 |
80 | ### Magic method access
81 |
82 | The options flag `JSONPath::ALLOW_MAGIC` will instruct JSONPath when retrieving a value to first check if an object
83 | has a magic `__get()` method and will call this method if available. This feature is *iffy* and
84 | not very predictable as:
85 |
86 | - wildcard and recursive features will only look at public properties and can't smell which properties are magically accessible
87 | - there is no `property_exists` check for magic methods so an object with a magic `__get()` will always return `true` when checking
88 | if the property exists
89 | - any errors thrown or unpredictable behaviour caused by fetching via `__get()` is your own problem to deal with
90 |
91 | ```php
92 | $jsonPath = new JSONPath($myObject, JSONPath::ALLOW_MAGIC);
93 | ```
94 |
95 | For more examples, check the JSONPathTest.php tests file.
96 |
97 | Script expressions
98 | -------
99 |
100 | Script expressions are not supported as the original author intended because:
101 |
102 | - This would only be achievable through `eval` (boo).
103 | - Using the script engine from different languages defeats the purpose of having a single expression evaluate the same way in different
104 | languages which seems like a bit of a flaw if you're creating an abstract expression syntax.
105 |
106 | So here are the types of query expressions that are supported:
107 |
108 | [?(@._KEY_ _OPERATOR_ _VALUE_)] // <, >, !=, and ==
109 | Eg.
110 | [?(@.title == "A string")] //
111 | [?(@.title = "A string")]
112 | // A single equals is not an assignment but the SQL-style of '=='
113 |
114 | Known issues
115 | ------
116 | - This project has not implemented multiple string indexes eg. `$[name,year]` or `$["name","year"]`. I have no ETA on that feature and it would require some re-writing of the parser that uses a very basic regex implementation.
117 |
118 | Similar projects
119 | ----------------
120 | [Galbar/JsonPath-PHP](https://github.com/Galbar/JsonPath-PHP) is a PHP implementation that does a few things this project doesn't and is a strong alternative
121 |
122 | [JMESPath](https://github.com/jmespath) does similiar things, is full of features and has a PHP implementation
123 |
124 | The [Hash](http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html) utility from CakePHP does some similar things
125 |
126 | The original JsonPath implementations is available at [http://code.google.com/p/jsonpath]() and re-hosted for composer
127 | here [Peekmo/JsonPath](https://github.com/Peekmo/JsonPath).
128 |
129 | [ObjectPath](http://objectpath.org) ([https://github.com/adriank/ObjectPath]()) appears to be a Python/JS implementation
130 | with a new name and extra features.
131 |
132 | Changelog
133 | ---------
134 | ### 0.5.0
135 | - Fixed the slice notation (eg. [0:2:5] etc.). **Breaks code relying on the broken implementation**
136 |
137 | ### 0.3.0
138 | - Added JSONPathToken class as value object
139 | - Lexer clean up and refactor
140 | - Updated the lexing and filtering of the recursive token ("..") to allow for a combination of recursion
141 | and filters, eg. $..[?(@.type == 'suburb')].name
142 |
143 | ### 0.2.1 - 0.2.5
144 | - Various bug fixes and clean up
145 |
146 | ### 0.2.0
147 | - Added a heap of array access features for more creative iterating and chaining possibilities
148 |
149 | ### 0.1.x
150 | - Init
151 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flow/jsonpath",
3 | "abandoned": "softcreatr/jsonpath",
4 | "description": "JSONPath implementation for parsing, searching and flattening arrays",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Stephen Frank",
9 | "email": "stephen@flowsa.com"
10 | }
11 | ],
12 | "autoload": {
13 | "psr-0": {
14 | "Flow\\JSONPath": "src/",
15 | "Flow\\JSONPath\\Test": "tests/"
16 | }
17 | },
18 | "require": {
19 | "php": ">=5.4.0"
20 | },
21 | "require-dev": {
22 | "peekmo/jsonpath": "dev-master",
23 | "phpunit/phpunit": "^7.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 | ./tests/
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/Flow/JSONPath/AccessHelper.php:
--------------------------------------------------------------------------------
1 | __get($key);
43 | }
44 |
45 | if (is_object($collection) && !$collection instanceof \ArrayAccess) {
46 | return $collection->$key;
47 | }
48 |
49 | if (is_array($collection)) {
50 | if (is_int($key)) {
51 | return array_slice($collection, $key, 1, false)[0];
52 | } else {
53 | return $collection[$key];
54 | }
55 | }
56 |
57 | if (is_object($collection) && !$collection instanceof \ArrayAccess) {
58 | return $collection->$key;
59 | }
60 |
61 | /*
62 | * Find item in php collection by index
63 | * Written this way to handle instances ArrayAccess or Traversable objects
64 | */
65 | if (is_int($key)) {
66 | $i = 0;
67 | foreach ($collection as $val) {
68 | if ($i === $key) {
69 | return $val;
70 | }
71 | $i += 1;
72 | }
73 | if ($key < 0) {
74 | $total = $i;
75 | $i = 0;
76 | foreach ($collection as $val) {
77 | if ($i - $total === $key) {
78 | return $val;
79 | }
80 | $i += 1;
81 | }
82 | }
83 | }
84 |
85 | // Finally, try anything
86 | return $collection[$key];
87 | }
88 |
89 | public static function setValue(&$collection, $key, $value)
90 | {
91 | if (is_object($collection) && ! $collection instanceof \ArrayAccess) {
92 | return $collection->$key = $value;
93 | } else {
94 | return $collection[$key] = $value;
95 | }
96 | }
97 |
98 | public static function unsetValue(&$collection, $key)
99 | {
100 | if (is_object($collection) && ! $collection instanceof \ArrayAccess) {
101 | unset($collection->$key);
102 | } else {
103 | unset($collection[$key]);
104 | }
105 | }
106 |
107 | public static function arrayValues($collection)
108 | {
109 | if (is_array($collection)) {
110 | return array_values($collection);
111 | } else if (is_object($collection)) {
112 | return array_values((array) $collection);
113 | }
114 |
115 | throw new JSONPathException("Invalid variable type for arrayValues");
116 | }
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/src/Flow/JSONPath/Filters/AbstractFilter.php:
--------------------------------------------------------------------------------
1 | token = $token;
24 | $this->options = $options;
25 | $this->magicIsAllowed = $this->options & JSONPath::ALLOW_MAGIC;
26 | }
27 |
28 | public function isMagicAllowed()
29 | {
30 | return $this->magicIsAllowed;
31 | }
32 |
33 | /**
34 | * @param $collection
35 | * @return array
36 | */
37 | abstract public function filter($collection);
38 | }
39 |
--------------------------------------------------------------------------------
/src/Flow/JSONPath/Filters/IndexFilter.php:
--------------------------------------------------------------------------------
1 | token->value, $this->magicIsAllowed)) {
15 | return [
16 | AccessHelper::getValue($collection, $this->token->value, $this->magicIsAllowed)
17 | ];
18 | } else if ($this->token->value === "*") {
19 | return AccessHelper::arrayValues($collection);
20 | }
21 |
22 | return [];
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/Flow/JSONPath/Filters/IndexesFilter.php:
--------------------------------------------------------------------------------
1 | token->value as $index) {
16 | if (AccessHelper::keyExists($collection, $index, $this->magicIsAllowed)) {
17 | $return[] = AccessHelper::getValue($collection, $index, $this->magicIsAllowed);
18 | }
19 | }
20 | return $return;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Flow/JSONPath/Filters/QueryMatchFilter.php:
--------------------------------------------------------------------------------
1 | \w+)|\[["\']?(?.*?)["\']?\])
10 | (\s*(?==|=|<>|!==|!=|>|<)\s*(?.+))?
11 | ';
12 |
13 | /**
14 | * @param array $collection
15 | * @throws \Exception
16 | * @return array
17 | */
18 | public function filter($collection)
19 | {
20 | $return = [];
21 |
22 | preg_match('/^' . static::MATCH_QUERY_OPERATORS . '$/x', $this->token->value, $matches);
23 |
24 | if (!isset($matches[1])) {
25 | throw new \Exception("Malformed filter query");
26 | }
27 |
28 | $key = $matches['key'] ?: $matches['keySquare'];
29 |
30 | if ($key === "") {
31 | throw new \Exception("Malformed filter query: key was not set");
32 | }
33 |
34 | $operator = isset($matches['operator']) ? $matches['operator'] : null;
35 | $comparisonValue = isset($matches['comparisonValue']) ? $matches['comparisonValue'] : null;
36 |
37 | if (strtolower($comparisonValue) === "false") {
38 | $comparisonValue = false;
39 | }
40 | if (strtolower($comparisonValue) === "true") {
41 | $comparisonValue = true;
42 | }
43 | if (strtolower($comparisonValue) === "null") {
44 | $comparisonValue = null;
45 | }
46 |
47 | $comparisonValue = preg_replace('/^[\'"]/', '', $comparisonValue);
48 | $comparisonValue = preg_replace('/[\'"]$/', '', $comparisonValue);
49 |
50 | foreach ($collection as $value) {
51 | if (AccessHelper::keyExists($value, $key, $this->magicIsAllowed)) {
52 | $value1 = AccessHelper::getValue($value, $key, $this->magicIsAllowed);
53 |
54 | if ($operator === null && AccessHelper::keyExists($value, $key, $this->magicIsAllowed)) {
55 | $return[] = $value;
56 | }
57 |
58 | if (($operator === "=" || $operator === "==") && $value1 == $comparisonValue) {
59 | $return[] = $value;
60 | }
61 | if (($operator === "!=" || $operator === "!==" || $operator === "<>") && $value1 != $comparisonValue) {
62 | $return[] = $value;
63 | }
64 | if ($operator == ">" && $value1 > $comparisonValue) {
65 | $return[] = $value;
66 | }
67 | if ($operator == "<" && $value1 < $comparisonValue) {
68 | $return[] = $value;
69 | }
70 | }
71 | }
72 |
73 | return $return;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Flow/JSONPath/Filters/QueryResultFilter.php:
--------------------------------------------------------------------------------
1 | \w+)\s*(?-|\+|\*|\/)\s*(?\d+)/', $this->token->value, $matches);
19 |
20 | $matchKey = $matches['key'];
21 |
22 | if (AccessHelper::keyExists($collection, $matchKey, $this->magicIsAllowed)) {
23 | $value = AccessHelper::getValue($collection, $matchKey, $this->magicIsAllowed);
24 | } else {
25 | if ($matches['key'] === 'length') {
26 | $value = count($collection);
27 | } else {
28 | return;
29 | }
30 | }
31 |
32 | switch ($matches['operator']) {
33 | case '+':
34 | $resultKey = $value + $matches['numeric'];
35 | break;
36 | case '*':
37 | $resultKey = $value * $matches['numeric'];
38 | break;
39 | case '-':
40 | $resultKey = $value - $matches['numeric'];
41 | break;
42 | case '/':
43 | $resultKey = $value / $matches['numeric'];
44 | break;
45 | default:
46 | throw new JSONPathException("Unsupported operator in expression");
47 | break;
48 | }
49 |
50 | if (AccessHelper::keyExists($collection, $resultKey, $this->magicIsAllowed)) {
51 | $result[] = AccessHelper::getValue($collection, $resultKey, $this->magicIsAllowed);
52 | }
53 |
54 | return $result;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Flow/JSONPath/Filters/RecursiveFilter.php:
--------------------------------------------------------------------------------
1 | recurse($result, $collection);
18 |
19 | return $result;
20 | }
21 |
22 | private function recurse(& $result, $data)
23 | {
24 | $result[] = $data;
25 |
26 | if (AccessHelper::isCollectionType($data)) {
27 | foreach (AccessHelper::arrayValues($data) as $key => $value) {
28 | $results[] = $value;
29 |
30 | if (AccessHelper::isCollectionType($value)) {
31 | $this->recurse($result, $value);
32 | }
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Flow/JSONPath/Filters/SliceFilter.php:
--------------------------------------------------------------------------------
1 | token->value['start'];
19 | $end = $this->token->value['end'];
20 | $step = $this->token->value['step'] ?: 1;
21 |
22 | if ($start === null) {
23 | $start = 0;
24 | }
25 |
26 | if ($start < 0) {
27 | $start = $length + $start;
28 | }
29 |
30 | if ($end === null) {
31 | // negative index start means the end is -1, else the end is the last index
32 | $end = $length;
33 | }
34 |
35 | if ($end < 0) {
36 | $end = $length + $end;
37 | }
38 |
39 | for ($i = $start; $i < $end; $i += $step) {
40 | $index = $i;
41 |
42 | if ($i < 0) {
43 | $index = $length + $i;
44 | }
45 |
46 | if (AccessHelper::keyExists($collection, $index, $this->magicIsAllowed)) {
47 | $result[] = $collection[$index];
48 | }
49 | }
50 |
51 | return $result;
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/src/Flow/JSONPath/JSONPath.php:
--------------------------------------------------------------------------------
1 | data = $data;
27 | $this->options = $options;
28 | }
29 |
30 | /**
31 | * Evaluate an expression
32 | *
33 | * @param $expression
34 | * @return static
35 | * @throws JSONPathException
36 | */
37 | public function find($expression)
38 | {
39 | $tokens = $this->parseTokens($expression);
40 |
41 | $collectionData = [$this->data];
42 |
43 | foreach ($tokens as $token) {
44 | $filter = $token->buildFilter($this->options);
45 |
46 | $filteredData = [];
47 |
48 | foreach ($collectionData as $value) {
49 | if (AccessHelper::isCollectionType($value)) {
50 | $filteredValue = $filter->filter($value);
51 | $filteredData = array_merge($filteredData, $filteredValue);
52 | }
53 | }
54 |
55 | $collectionData = $filteredData;
56 | }
57 |
58 |
59 | return new static($collectionData, $this->options);
60 | }
61 |
62 | /**
63 | * @return mixed
64 | */
65 | public function first()
66 | {
67 | $keys = AccessHelper::collectionKeys($this->data);
68 |
69 | if (empty($keys)) {
70 | return null;
71 | }
72 |
73 | $value = isset($this->data[$keys[0]]) ? $this->data[$keys[0]] : null;
74 |
75 | return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value;
76 | }
77 |
78 | /**
79 | * Evaluate an expression and return the last result
80 | * @return mixed
81 | */
82 | public function last()
83 | {
84 | $keys = AccessHelper::collectionKeys($this->data);
85 |
86 | if (empty($keys)) {
87 | return null;
88 | }
89 |
90 | $value = $this->data[end($keys)] ? $this->data[end($keys)] : null;
91 |
92 | return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value;
93 | }
94 |
95 | /**
96 | * Evaluate an expression and return the first key
97 | * @return mixed
98 | */
99 | public function firstKey()
100 | {
101 | $keys = AccessHelper::collectionKeys($this->data);
102 |
103 | if (empty($keys)) {
104 | return null;
105 | }
106 |
107 | return $keys[0];
108 | }
109 |
110 | /**
111 | * Evaluate an expression and return the last key
112 | * @return mixed
113 | */
114 | public function lastKey()
115 | {
116 | $keys = AccessHelper::collectionKeys($this->data);
117 |
118 | if (empty($keys) || end($keys) === false) {
119 | return null;
120 | }
121 |
122 | return end($keys);
123 | }
124 |
125 | /**
126 | * @param $expression
127 | * @return array
128 | * @throws \Exception
129 | */
130 | public function parseTokens($expression)
131 | {
132 | $cacheKey = md5($expression);
133 |
134 | if (isset(static::$tokenCache[$cacheKey])) {
135 | return static::$tokenCache[$cacheKey];
136 | }
137 |
138 | $lexer = new JSONPathLexer($expression);
139 |
140 | $tokens = $lexer->parseExpression();
141 |
142 | static::$tokenCache[$cacheKey] = $tokens;
143 |
144 | return $tokens;
145 | }
146 |
147 | /**
148 | * @return mixed
149 | */
150 | public function data()
151 | {
152 | return $this->data;
153 | }
154 |
155 | public function offsetExists($offset)
156 | {
157 | return AccessHelper::keyExists($this->data, $offset);
158 | }
159 |
160 | public function offsetGet($offset)
161 | {
162 | $value = AccessHelper::getValue($this->data, $offset);
163 |
164 | return AccessHelper::isCollectionType($value)
165 | ? new static($value, $this->options)
166 | : $value;
167 | }
168 |
169 | public function offsetSet($offset, $value)
170 | {
171 | if ($offset === null) {
172 | $this->data[] = $value;
173 | } else {
174 | AccessHelper::setValue($this->data, $offset, $value);
175 | }
176 | }
177 |
178 | public function offsetUnset($offset)
179 | {
180 | AccessHelper::unsetValue($this->data, $offset);
181 | }
182 |
183 | public function jsonSerialize()
184 | {
185 | return $this->data;
186 | }
187 |
188 | /**
189 | * Return the current element
190 | */
191 | public function current()
192 | {
193 | $value = current($this->data);
194 |
195 | return AccessHelper::isCollectionType($value) ? new static($value, $this->options) : $value;
196 | }
197 |
198 | /**
199 | * Move forward to next element
200 | */
201 | public function next()
202 | {
203 | next($this->data);
204 | }
205 |
206 | /**
207 | * Return the key of the current element
208 | */
209 | public function key()
210 | {
211 | return key($this->data);
212 | }
213 |
214 | /**
215 | * Checks if current position is valid
216 | */
217 | public function valid()
218 | {
219 | return key($this->data) !== null;
220 | }
221 |
222 | /**
223 | * Rewind the Iterator to the first element
224 | */
225 | public function rewind()
226 | {
227 | reset($this->data);
228 | }
229 |
230 | /**
231 | * @param $key
232 | * @return JSONPath|mixed|null|static
233 | */
234 | public function __get($key)
235 | {
236 | return $this->offsetExists($key) ? $this->offsetGet($key) : null;
237 | }
238 |
239 | /**
240 | * Count elements of an object
241 | */
242 | public function count()
243 | {
244 | return count($this->data);
245 | }
246 | }
247 |
--------------------------------------------------------------------------------
/src/Flow/JSONPath/JSONPathException.php:
--------------------------------------------------------------------------------
1 | expression = $expression;
57 | $this->expressionLength = $len;
58 | }
59 |
60 | /**
61 | * @return array
62 | * @throws JSONPathException
63 | */
64 | public function parseExpressionTokens()
65 | {
66 | $dotIndexDepth = 0;
67 | $squareBracketDepth = 0;
68 | $tokenValue = '';
69 | $tokens = [];
70 |
71 | for ($i = 0; $i < $this->expressionLength; $i++) {
72 | $char = $this->expression[$i];
73 |
74 | if ($squareBracketDepth === 0) {
75 | if ($char === '.') {
76 |
77 | if ($this->lookAhead($i, 1) === ".") {
78 | $tokens[] = new JSONPathToken(JSONPathToken::T_RECURSIVE, null);
79 | }
80 |
81 | continue;
82 | }
83 | }
84 |
85 | if ($char === '[') {
86 | $squareBracketDepth += 1;
87 |
88 | if ($squareBracketDepth === 1) {
89 | continue;
90 | }
91 | }
92 |
93 | if ($char === ']') {
94 | $squareBracketDepth -= 1;
95 |
96 | if ($squareBracketDepth === 0) {
97 | continue;
98 | }
99 | }
100 |
101 | /*
102 | * Within square brackets
103 | */
104 | if ($squareBracketDepth > 0) {
105 | $tokenValue .= $char;
106 | if ($this->lookAhead($i, 1) === ']' && $squareBracketDepth === 1) {
107 | $tokens[] = $this->createToken($tokenValue);
108 | $tokenValue = '';
109 | }
110 | }
111 |
112 | /*
113 | * Outside square brackets
114 | */
115 | if ($squareBracketDepth === 0) {
116 | $tokenValue .= $char;
117 |
118 | // Double dot ".."
119 | if ($char === "." && $dotIndexDepth > 1) {
120 | $tokens[] = $this->createToken($tokenValue);
121 | $tokenValue = '';
122 | continue;
123 | }
124 |
125 | if ($this->lookAhead($i, 1) === '.' || $this->lookAhead($i, 1) === '[' || $this->atEnd($i)) {
126 | $tokens[] = $this->createToken($tokenValue);
127 | $tokenValue = '';
128 | $dotIndexDepth -= 1;
129 | }
130 | }
131 |
132 | }
133 |
134 | if ($tokenValue !== '') {
135 | $tokens[] = $this->createToken($tokenValue);
136 | }
137 |
138 | return $tokens;
139 | }
140 |
141 | protected function lookAhead($pos, $forward = 1)
142 | {
143 | return isset($this->expression[$pos + $forward]) ? $this->expression[$pos + $forward] : null;
144 | }
145 |
146 | protected function atEnd($pos)
147 | {
148 | return $pos === $this->expressionLength;
149 | }
150 |
151 |
152 |
153 | public function parseExpression()
154 | {
155 | $tokens = $this->parseExpressionTokens();
156 |
157 | return $tokens;
158 | }
159 |
160 | /**
161 | * @param $value
162 | * @return string
163 | * @throws JSONPathException
164 | */
165 | protected function createToken($value)
166 | {
167 | if (preg_match('/^(' . static::MATCH_INDEX . ')$/xu', $value, $matches)) {
168 | if (preg_match('/^-?\d+$/', $value)) {
169 | $value = (int)$value;
170 | }
171 | return new JSONPathToken(JSONPathToken::T_INDEX, $value);
172 | }
173 |
174 | if (preg_match('/^' . static::MATCH_INDEXES . '$/xu', $value, $matches)) {
175 | $value = explode(',', trim($value, ','));
176 |
177 | foreach ($value as $i => $v) {
178 | $value[$i] = (int) trim($v);
179 | }
180 |
181 | return new JSONPathToken(JSONPathToken::T_INDEXES, $value);
182 | }
183 |
184 | if (preg_match('/^' . static::MATCH_SLICE . '$/xu', $value, $matches)) {
185 | $parts = explode(':', $value);
186 |
187 | $value = [
188 | 'start' => isset($parts[0]) && $parts[0] !== "" ? (int) $parts[0] : null,
189 | 'end' => isset($parts[1]) && $parts[1] !== "" ? (int) $parts[1] : null,
190 | 'step' => isset($parts[2]) && $parts[2] !== "" ? (int) $parts[2] : null,
191 | ];
192 |
193 | return new JSONPathToken(JSONPathToken::T_SLICE, $value);
194 | }
195 |
196 | if (preg_match('/^' . static::MATCH_QUERY_RESULT . '$/xu', $value)) {
197 | $value = substr($value, 1, -1);
198 |
199 | return new JSONPathToken(JSONPathToken::T_QUERY_RESULT, $value);
200 | }
201 |
202 | if (preg_match('/^' . static::MATCH_QUERY_MATCH . '$/xu', $value)) {
203 | $value = substr($value, 2, -1);
204 |
205 | return new JSONPathToken(JSONPathToken::T_QUERY_MATCH, $value);
206 | }
207 |
208 | if (preg_match('/^' . static::MATCH_INDEX_IN_SINGLE_QUOTES . '$/xu', $value, $matches)) {
209 | $value = $matches[1];
210 | $value = trim($value);
211 |
212 | return new JSONPathToken(JSONPathToken::T_INDEX, $value);
213 | }
214 |
215 | if (preg_match('/^' . static::MATCH_INDEX_IN_DOUBLE_QUOTES . '$/xu', $value, $matches)) {
216 | $value = $matches[1];
217 | $value = trim($value);
218 |
219 | return new JSONPathToken(JSONPathToken::T_INDEX, $value);
220 | }
221 |
222 | throw new JSONPathException("Unable to parse token {$value} in expression: $this->expression");
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/src/Flow/JSONPath/JSONPathToken.php:
--------------------------------------------------------------------------------
1 | validateType($type);
30 |
31 | $this->type = $type;
32 | $this->value = $value;
33 | }
34 |
35 | public function validateType($type)
36 | {
37 | if (!in_array($type, static::getTypes(), true)) {
38 | throw new JSONPathException('Invalid token: ' . $type);
39 | }
40 | }
41 |
42 | public static function getTypes()
43 | {
44 | return [
45 | static::T_INDEX,
46 | static::T_RECURSIVE,
47 | static::T_QUERY_RESULT,
48 | static::T_QUERY_MATCH,
49 | static::T_SLICE,
50 | static::T_INDEXES,
51 | ];
52 | }
53 |
54 |
55 | /**
56 | * @param $token
57 | * @return AbstractFilter
58 | * @throws \Exception
59 | */
60 | public function buildFilter($options)
61 | {
62 | $filterClass = 'Flow\\JSONPath\\Filters\\' . ucfirst($this->type) . 'Filter';
63 |
64 | if (! class_exists($filterClass)) {
65 | throw new JSONPathException("No filter class exists for token [{$this->type}]");
66 | }
67 |
68 | return new $filterClass($this, $options);
69 | }
70 |
71 | }
--------------------------------------------------------------------------------
/tests/JSONPathArrayAccessTest.php:
--------------------------------------------------------------------------------
1 | exampleData(rand(0, 1));
14 |
15 | $conferences = (new JSONPath($data))->find('.conferences.*');
16 | $teams = $conferences->find('..teams.*');
17 |
18 | $this->assertEquals('Dodger', $teams[0]['name']);
19 | $this->assertEquals('Mets', $teams[1]['name']);
20 |
21 | $teams = (new JSONPath($data))->find('.conferences.*')->find('..teams.*');
22 |
23 | $this->assertEquals('Dodger', $teams[0]['name']);
24 | $this->assertEquals('Mets', $teams[1]['name']);
25 |
26 | $teams = (new JSONPath($data))->find('.conferences..teams.*');
27 |
28 | $this->assertEquals('Dodger', $teams[0]['name']);
29 | $this->assertEquals('Mets', $teams[1]['name']);
30 | }
31 |
32 | public function testIterating()
33 | {
34 | $data = $this->exampleData(rand(0, 1));
35 |
36 | $conferences = (new JSONPath($data))->find('.conferences.*');
37 |
38 | $names = [];
39 |
40 | foreach ($conferences as $conference) {
41 | $players = $conference->find('.teams.*.players[?(@.active=yes)]');
42 |
43 | foreach ($players as $player) {
44 | $names[] = $player->name;
45 | }
46 | }
47 |
48 | $this->assertEquals(['Joe Face', 'something'], $names);
49 | }
50 |
51 | public function testDifferentStylesOfAccess()
52 | {
53 | $data = $this->exampleData(rand(0, 1));
54 |
55 | $league = new JSONPath($data);
56 |
57 | $conferences = $league->conferences;
58 | $firstConference = $league->conferences[0];
59 |
60 | $this->assertEquals('Western Conference', $firstConference->name);
61 | }
62 |
63 | public function exampleData($asArray = true)
64 | {
65 | $data = [
66 | 'name' => 'Major League Baseball',
67 | 'abbr' => 'MLB',
68 | 'conferences' => [
69 | [
70 | 'name' => 'Western Conference',
71 | 'abbr' => 'West',
72 | 'teams' => [
73 | [
74 | 'name' => 'Dodger',
75 | 'city' => 'Los Angeles',
76 | 'whatever' => 'else',
77 | 'players' => [
78 | ['name' => 'Bob Smith', 'number' => 22],
79 | ['name' => 'Joe Face', 'number' => 23, 'active' => 'yes'],
80 | ],
81 | ]
82 | ],
83 | ],
84 | [
85 | 'name' => 'Eastern Conference',
86 | 'abbr' => 'East',
87 | 'teams' => [
88 | [
89 | 'name' => 'Mets',
90 | 'city' => 'New York',
91 | 'whatever' => 'else',
92 | 'players' => [
93 | ['name' => 'something', 'number' => 14, 'active' => 'yes'],
94 | ['name' => 'something', 'number' => 15],
95 | ]
96 | ]
97 | ]
98 | ]
99 | ]
100 | ];
101 |
102 | return $asArray ? $data : json_decode(json_encode($data));
103 |
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/tests/JSONPathDashedIndexTest.php:
--------------------------------------------------------------------------------
1 | [
19 | 'test-test-test' => 'foo'
20 | ]
21 | ],
22 | [
23 | 'foo'
24 | ]
25 | ],
26 | [
27 | '$.data[40f35757-2563-4790-b0b1-caa904be455f]',
28 | [
29 | 'data' => [
30 | '40f35757-2563-4790-b0b1-caa904be455f' => 'bar'
31 | ]
32 | ],
33 | [
34 | 'bar'
35 | ]
36 | ]
37 | ];
38 | }
39 |
40 | /** @dataProvider indexDataProvider */
41 | public function testSlice($path, $data, $expected)
42 | {
43 | $jsonPath = new JSONPath($data);
44 | $result = $jsonPath->find($path)->data();
45 | $this->assertEquals($expected, $result);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/JSONPathLexerTest.php:
--------------------------------------------------------------------------------
1 | parseExpression();
15 | $this->assertEquals(JSONPathToken::T_INDEX, $tokens[0]->type);
16 | $this->assertEquals("*", $tokens[0]->value);
17 | }
18 |
19 | public function test_Index_Simple()
20 | {
21 | $tokens = (new JSONPathLexer('.foo'))->parseExpression();
22 | $this->assertEquals(JSONPathToken::T_INDEX, $tokens[0]->type);
23 | $this->assertEquals("foo", $tokens[0]->value);
24 | }
25 |
26 | public function test_Index_Recursive()
27 | {
28 | $tokens = (new JSONPathLexer('..teams.*'))->parseExpression();
29 |
30 | $this->assertEquals(3, count($tokens));
31 |
32 | $this->assertEquals(JSONPathToken::T_RECURSIVE, $tokens[0]->type);
33 | $this->assertEquals(null, $tokens[0]->value);
34 | $this->assertEquals(JSONPathToken::T_INDEX, $tokens[1]->type);
35 | $this->assertEquals('teams', $tokens[1]->value);
36 | $this->assertEquals(JSONPathToken::T_INDEX, $tokens[2]->type);
37 | $this->assertEquals('*', $tokens[2]->value);
38 | }
39 |
40 | public function test_Index_Complex()
41 | {
42 | $tokens = (new JSONPathLexer('["\'b.^*_"]'))->parseExpression();
43 | $this->assertEquals(JSONPathToken::T_INDEX, $tokens[0]->type);
44 | $this->assertEquals("'b.^*_", $tokens[0]->value);
45 | }
46 |
47 | /**
48 | * @expectedException Flow\JSONPath\JSONPathException
49 | * @expectedExceptionMessage Unable to parse token hello* in expression: .hello*
50 | */
51 | public function test_Index_BadlyFormed()
52 | {
53 | $tokens = (new JSONPathLexer('.hello*'))->parseExpression();
54 | }
55 |
56 | public function test_Index_Integer()
57 | {
58 | $tokens = (new \Flow\JSONPath\JSONPathLexer('[0]'))->parseExpression();
59 | $this->assertEquals(JSONPathToken::T_INDEX, $tokens[0]->type);
60 | $this->assertEquals("0", $tokens[0]->value);
61 | }
62 |
63 | public function test_Index_IntegerAfterDotNotation()
64 | {
65 | $tokens = (new \Flow\JSONPath\JSONPathLexer('.books[0]'))->parseExpression();
66 | $this->assertEquals(JSONPathToken::T_INDEX, $tokens[0]->type);
67 | $this->assertEquals(JSONPathToken::T_INDEX, $tokens[1]->type);
68 | $this->assertEquals("books", $tokens[0]->value);
69 | $this->assertEquals("0", $tokens[1]->value);
70 | }
71 |
72 | public function test_Index_Word()
73 | {
74 | $tokens = (new \Flow\JSONPath\JSONPathLexer('["foo$-/\'"]'))->parseExpression();
75 | $this->assertEquals(JSONPathToken::T_INDEX, $tokens[0]->type);
76 | $this->assertEquals("foo$-/'", $tokens[0]->value);
77 | }
78 |
79 | public function test_Index_WordWithWhitespace()
80 | {
81 | $tokens = (new \Flow\JSONPath\JSONPathLexer('[ "foo$-/\'" ]'))->parseExpression();
82 | $this->assertEquals(JSONPathToken::T_INDEX, $tokens[0]->type);
83 | $this->assertEquals("foo$-/'", $tokens[0]->value);
84 | }
85 |
86 | public function test_Slice_Simple()
87 | {
88 | $tokens = (new \Flow\JSONPath\JSONPathLexer('[0:1:2]'))->parseExpression();
89 | $this->assertEquals(JSONPathToken::T_SLICE, $tokens[0]->type);
90 | $this->assertEquals(['start' => 0, 'end' => 1, 'step' => 2], $tokens[0]->value);
91 | }
92 |
93 | public function test_Index_NegativeIndex()
94 | {
95 | $tokens = (new \Flow\JSONPath\JSONPathLexer('[-1]'))->parseExpression();
96 | $this->assertEquals(JSONPathToken::T_SLICE, $tokens[0]->type);
97 | $this->assertEquals(['start' => -1, 'end' => null, 'step' => null], $tokens[0]->value);
98 | }
99 |
100 | public function test_Slice_AllNull()
101 | {
102 | $tokens = (new \Flow\JSONPath\JSONPathLexer('[:]'))->parseExpression();
103 | $this->assertEquals(JSONPathToken::T_SLICE, $tokens[0]->type);
104 | $this->assertEquals(['start' => null, 'end' => null, 'step' => null], $tokens[0]->value);
105 | }
106 |
107 | public function test_QueryResult_Simple()
108 | {
109 | $tokens = (new \Flow\JSONPath\JSONPathLexer('[(@.foo + 2)]'))->parseExpression();
110 | $this->assertEquals(JSONPathToken::T_QUERY_RESULT, $tokens[0]->type);
111 | $this->assertEquals('@.foo + 2', $tokens[0]->value);
112 | }
113 |
114 | public function test_QueryMatch_Simple()
115 | {
116 | $tokens = (new \Flow\JSONPath\JSONPathLexer('[?(@.foo < \'bar\')]'))->parseExpression();
117 | $this->assertEquals(JSONPathToken::T_QUERY_MATCH, $tokens[0]->type);
118 | $this->assertEquals('@.foo < \'bar\'', $tokens[0]->value);
119 | }
120 |
121 | public function test_QueryMatch_NotEqualTO()
122 | {
123 | $tokens = (new \Flow\JSONPath\JSONPathLexer('[?(@.foo != \'bar\')]'))->parseExpression();
124 | $this->assertEquals(JSONPathToken::T_QUERY_MATCH, $tokens[0]->type);
125 | $this->assertEquals('@.foo != \'bar\'', $tokens[0]->value);
126 | }
127 |
128 | public function test_QueryMatch_Brackets()
129 | {
130 | $tokens = (new \Flow\JSONPath\JSONPathLexer("[?(@['@language']='en')]"))->parseExpression();
131 |
132 | $this->assertEquals(JSONPathToken::T_QUERY_MATCH, $tokens[0]->type);
133 | $this->assertEquals("@['@language']='en'", $tokens[0]->value);
134 |
135 | }
136 |
137 | public function test_Recursive_Simple()
138 | {
139 | $tokens = (new \Flow\JSONPath\JSONPathLexer('..foo'))->parseExpression();
140 | $this->assertEquals(JSONPathToken::T_RECURSIVE, $tokens[0]->type);
141 | $this->assertEquals(JSONPathToken::T_INDEX, $tokens[1]->type);
142 | $this->assertEquals(null, $tokens[0]->value);
143 | $this->assertEquals('foo', $tokens[1]->value);
144 | }
145 |
146 | public function test_Recursive_Wildcard()
147 | {
148 | $tokens = (new \Flow\JSONPath\JSONPathLexer('..*'))->parseExpression();
149 | $this->assertEquals(JSONPathToken::T_RECURSIVE, $tokens[0]->type);
150 | $this->assertEquals(JSONPathToken::T_INDEX, $tokens[1]->type);
151 | $this->assertEquals(null, $tokens[0]->value);
152 | $this->assertEquals('*', $tokens[1]->value);
153 | }
154 |
155 | /**
156 | * @expectedException Flow\JSONPath\JSONPathException
157 | * @expectedExceptionMessage Unable to parse token ba^r in expression: ..ba^r
158 | */
159 | public function test_Recursive_BadlyFormed()
160 | {
161 | $tokens = (new JSONPathLexer('..ba^r'))->parseExpression();
162 | }
163 |
164 | /**
165 | */
166 | public function test_Indexes_Simple()
167 | {
168 | $tokens = (new JSONPathLexer('[1,2,3]'))->parseExpression();
169 | $this->assertEquals(JSONPathToken::T_INDEXES, $tokens[0]->type);
170 | $this->assertEquals([1,2,3], $tokens[0]->value);
171 | }
172 | /**
173 | */
174 | public function test_Indexes_Whitespace()
175 | {
176 | $tokens = (new JSONPathLexer('[ 1,2 , 3]'))->parseExpression();
177 | $this->assertEquals(JSONPathToken::T_INDEXES, $tokens[0]->type);
178 | $this->assertEquals([1,2,3], $tokens[0]->value);
179 | }
180 |
181 |
182 |
183 | }
184 |
--------------------------------------------------------------------------------
/tests/JSONPathSliceAccessTest.php:
--------------------------------------------------------------------------------
1 | [
19 | 'foo0',
20 | 'foo1',
21 | 'foo2',
22 | 'foo3',
23 | 'foo4',
24 | 'foo5',
25 | ]
26 | ],
27 | [
28 | 'foo1',
29 | 'foo2',
30 | ]
31 | ],
32 | [
33 | '$.data[4:]',
34 | [
35 | 'data' => [
36 | 'foo0',
37 | 'foo1',
38 | 'foo2',
39 | 'foo3',
40 | 'foo4',
41 | 'foo5',
42 | ]
43 | ],
44 | [
45 | 'foo4',
46 | 'foo5',
47 | ]
48 | ],
49 | [
50 | '$.data[:2]',
51 | [
52 | 'data' => [
53 | 'foo0',
54 | 'foo1',
55 | 'foo2',
56 | 'foo3',
57 | 'foo4',
58 | 'foo5',
59 | ]
60 | ],
61 | [
62 | 'foo0',
63 | 'foo1',
64 | ]
65 | ],
66 | [
67 | '$.data[:]',
68 | [
69 | 'data' => [
70 | 'foo0',
71 | 'foo1',
72 | 'foo2',
73 | 'foo3',
74 | 'foo4',
75 | 'foo5',
76 | ]
77 | ],
78 | [
79 | 'foo0',
80 | 'foo1',
81 | 'foo2',
82 | 'foo3',
83 | 'foo4',
84 | 'foo5',
85 | ]
86 | ],
87 | [
88 | '$.data[-1]',
89 | [
90 | 'data' => [
91 | 'foo0',
92 | 'foo1',
93 | 'foo2',
94 | 'foo3',
95 | 'foo4',
96 | 'foo5',
97 | ]
98 | ],
99 | [
100 | 'foo5',
101 | ]
102 | ],
103 | [
104 | '$.data[-2:]',
105 | [
106 | 'data' => [
107 | 'foo0',
108 | 'foo1',
109 | 'foo2',
110 | 'foo3',
111 | 'foo4',
112 | 'foo5',
113 | ]
114 | ],
115 | [
116 | 'foo4',
117 | 'foo5',
118 | ]
119 | ],
120 | [
121 | '$.data[:-2]',
122 | [
123 | 'data' => [
124 | 'foo0',
125 | 'foo1',
126 | 'foo2',
127 | 'foo3',
128 | 'foo4',
129 | 'foo5',
130 | ]
131 | ],
132 | [
133 | 'foo0',
134 | 'foo1',
135 | 'foo2',
136 | 'foo3',
137 | ]
138 | ],
139 | [
140 | '$.data[::2]',
141 | [
142 | 'data' => [
143 | 'foo0',
144 | 'foo1',
145 | 'foo2',
146 | 'foo3',
147 | 'foo4',
148 | 'foo5',
149 | ]
150 | ],
151 | [
152 | 'foo0',
153 | 'foo2',
154 | 'foo4'
155 | ]
156 | ],
157 | [
158 | '$.data[2::2]',
159 | [
160 | 'data' => [
161 | 'foo0',
162 | 'foo1',
163 | 'foo2',
164 | 'foo3',
165 | 'foo4',
166 | 'foo5',
167 | ]
168 | ],
169 | [
170 | 'foo2',
171 | 'foo4'
172 | ]
173 | ],
174 | [
175 | '$.data[:-2:2]',
176 | [
177 | 'data' => [
178 | 'foo0',
179 | 'foo1',
180 | 'foo2',
181 | 'foo3',
182 | 'foo4',
183 | 'foo5',
184 | ]
185 | ],
186 | [
187 | 'foo0',
188 | 'foo2'
189 | ]
190 | ],
191 | [
192 | '$.data[1:5:2]',
193 | [
194 | 'data' => [
195 | 'foo0',
196 | 'foo1',
197 | 'foo2',
198 | 'foo3',
199 | 'foo4',
200 | 'foo5',
201 | ]
202 | ],
203 | [
204 | 'foo1',
205 | 'foo3',
206 | ]
207 | ]
208 | ];
209 | }
210 |
211 | /** @dataProvider sliceDataProvider */
212 | public function testSlice($path, $data, $expected)
213 | {
214 | $jsonPath = new JSONPath($data);
215 | $result = $jsonPath->find($path)->data();
216 | $this->assertEquals($expected, $result);
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/tests/JSONPathTest.php:
--------------------------------------------------------------------------------
1 | exampleData(rand(0, 1))))->find('$.store.books[0].title');
19 | $this->assertEquals('Sayings of the Century', $result[0]);
20 | }
21 |
22 | /**
23 | * $['store']['books'][0]['title']
24 | */
25 | public function testChildOperatorsAlt()
26 | {
27 | $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$['store']['books'][0]['title']");
28 | $this->assertEquals('Sayings of the Century', $result[0]);
29 | }
30 |
31 | /**
32 | * $.array[start:end:step]
33 | */
34 | public function testFilterSliceA()
35 | {
36 | // Copy all items... similar to a wildcard
37 | $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$['store']['books'][:].title");
38 | $this->assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick', 'The Lord of the Rings'], $result->data());
39 | }
40 |
41 | /**
42 | * Positive end indexes
43 | * $[0:2]
44 | */
45 | public function testFilterSlice_PositiveEndIndexes()
46 | {
47 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:0]");
48 | $this->assertEquals([], $result->data());
49 |
50 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:1]");
51 | $this->assertEquals(["first"], $result->data());
52 |
53 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:2]");
54 | $this->assertEquals(["first", "second"], $result->data());
55 |
56 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[:2]");
57 | $this->assertEquals(["first", "second"], $result->data());
58 |
59 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[1:2]");
60 | $this->assertEquals(["second"], $result->data());
61 |
62 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:3:1]");
63 | $this->assertEquals(["first", "second","third"], $result->data());
64 |
65 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:3:0]");
66 | $this->assertEquals(["first", "second","third"], $result->data());
67 | }
68 |
69 | public function testFilterSlice_NegativeStartIndexes()
70 | {
71 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-2:]");
72 | $this->assertEquals(["fourth", "fifth"], $result->data());
73 |
74 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-1:]");
75 | $this->assertEquals(["fifth"], $result->data());
76 | }
77 |
78 | /**
79 | * Negative end indexes
80 | * $[:-2]
81 | */
82 | public function testFilterSlice_NegativeEndIndexes()
83 | {
84 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[:-2]");
85 | $this->assertEquals(["first", "second", "third"], $result->data());
86 |
87 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:-2]");
88 | $this->assertEquals(["first", "second", "third"], $result->data());
89 | }
90 |
91 | /**
92 | * Negative end indexes
93 | * $[:-2]
94 | */
95 | public function testFilterSlice_NegativeStartAndEndIndexes()
96 | {
97 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-2:-1]");
98 | $this->assertEquals(["fourth"], $result->data());
99 |
100 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-4:-2]");
101 | $this->assertEquals(["second", "third"], $result->data());
102 | }
103 |
104 | /**
105 | * Negative end indexes
106 | * $[:-2]
107 | */
108 | public function testFilterSlice_NegativeStartAndPositiveEnd()
109 | {
110 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-2:2]");
111 | $this->assertEquals([], $result->data());
112 | }
113 |
114 | public function testFilterSlice_StepBy2()
115 | {
116 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:4:2]");
117 | $this->assertEquals(["first", "third"], $result->data());
118 | }
119 |
120 |
121 | /**
122 | * The Last item
123 | * $[-1]
124 | */
125 | public function testFilterLastIndex()
126 | {
127 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-1]");
128 | $this->assertEquals(["fifth"], $result->data());
129 | }
130 |
131 | /**
132 | * Array index slice only end
133 | * $[:2]
134 | */
135 | public function testFilterSliceG()
136 | {
137 | // Fetch up to the second index
138 | $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[:2]");
139 | $this->assertEquals(["first", "second"], $result->data());
140 | }
141 |
142 | /**
143 | * $.store.books[(@.length-1)].title
144 | *
145 | * This notation is only partially implemented eg. hacked in
146 | */
147 | public function testChildQuery()
148 | {
149 | $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$.store.books[(@.length-1)].title");
150 | $this->assertEquals(['The Lord of the Rings'], $result->data());
151 | }
152 |
153 | /**
154 | * $.store.books[?(@.price < 10)].title
155 | * Filter books that have a price less than 10
156 | */
157 | public function testQueryMatchLessThan()
158 | {
159 | $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$.store.books[?(@.price < 10)].title");
160 | $this->assertEquals(['Sayings of the Century', 'Moby Dick'], $result->data());
161 | }
162 |
163 | /**
164 | * $..books[?(@.author == "J. R. R. Tolkien")]
165 | * Filter books that have a title equal to "..."
166 | */
167 | public function testQueryMatchEquals()
168 | {
169 | $results = (new JSONPath($this->exampleData(rand(0, 1))))->find('$..books[?(@.author == "J. R. R. Tolkien")].title');
170 | $this->assertEquals($results[0], 'The Lord of the Rings');
171 | }
172 |
173 | /**
174 | * $..books[?(@.author = 1)]
175 | * Filter books that have a title equal to "..."
176 | */
177 | public function testQueryMatchEqualsWithUnquotedInteger()
178 | {
179 | $results = (new JSONPath($this->exampleDataWithSimpleIntegers(rand(0, 1))))->find('$..features[?(@.value = 1)]');
180 | $this->assertEquals($results[0]->name, "foo");
181 | $this->assertEquals($results[1]->name, "baz");
182 | }
183 |
184 | /**
185 | * $..books[?(@.author != "J. R. R. Tolkien")]
186 | * Filter books that have a title not equal to "..."
187 | */
188 | public function testQueryMatchNotEqualsTo()
189 | {
190 | $results = (new JSONPath($this->exampleData(rand(0, 1))))->find('$..books[?(@.author != "J. R. R. Tolkien")].title');
191 | $this->assertcount(3, $results);
192 | $this->assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick'], [$results[0], $results[1], $results[2]]);
193 |
194 | $results = (new JSONPath($this->exampleData(rand(0, 1))))->find('$..books[?(@.author !== "J. R. R. Tolkien")].title');
195 | $this->assertcount(3, $results);
196 | $this->assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick'], [$results[0], $results[1], $results[2]]);
197 |
198 | $results = (new JSONPath($this->exampleData(rand(0, 1))))->find('$..books[?(@.author <> "J. R. R. Tolkien")].title');
199 | $this->assertcount(3, $results);
200 | $this->assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick'], [$results[0], $results[1], $results[2]]);
201 | }
202 |
203 | /**
204 | * $.store.books[*].author
205 | */
206 | public function testWildcardAltNotation()
207 | {
208 | $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$.store.books[*].author");
209 | $this->assertEquals(['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien'], $result->data());
210 | }
211 |
212 | /**
213 | * $..author
214 | */
215 | public function testRecursiveChildSearch()
216 | {
217 | $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..author");
218 | $this->assertEquals(['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien'], $result->data());
219 | }
220 |
221 | /**
222 | * $.store.*
223 | * all things in store
224 | * the structure of the example data makes this test look weird
225 | */
226 | public function testWildCard()
227 | {
228 | $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$.store.*");
229 | if (is_object($result[0][0])) {
230 | $this->assertEquals('Sayings of the Century', $result[0][0]->title);
231 | } else {
232 | $this->assertEquals('Sayings of the Century', $result[0][0]['title']);
233 | }
234 |
235 | if (is_object($result[1])) {
236 | $this->assertEquals('red', $result[1]->color);
237 | } else {
238 | $this->assertEquals('red', $result[1]['color']);
239 | }
240 | }
241 |
242 | /**
243 | * $.store..price
244 | * the price of everything in the store.
245 | */
246 | public function testRecursiveChildSearchAlt()
247 | {
248 | $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$.store..price");
249 | $this->assertEquals([8.95, 12.99, 8.99, 22.99, 19.95], $result->data());
250 | }
251 |
252 | /**
253 | * $..books[2]
254 | * the third book
255 | */
256 | public function testRecursiveChildSearchWithChildIndex()
257 | {
258 | $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..books[2].title");
259 | $this->assertEquals(["Moby Dick"], $result->data());
260 | }
261 |
262 | /**
263 | * $..books[(@.length-1)]
264 | */
265 | public function testRecursiveChildSearchWithChildQuery()
266 | {
267 | $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..books[(@.length-1)].title");
268 | $this->assertEquals(["The Lord of the Rings"], $result->data());
269 | }
270 |
271 | /**
272 | * $..books[-1:]
273 | * Resturn the last results
274 | */
275 | public function testRecursiveChildSearchWithSliceFilter()
276 | {
277 | $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..books[-1:].title");
278 | $this->assertEquals(["The Lord of the Rings"], $result->data());
279 | }
280 |
281 | /**
282 | * $..books[?(@.isbn)]
283 | * filter all books with isbn number
284 | */
285 | public function testRecursiveWithQueryMatch()
286 | {
287 | $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..books[?(@.isbn)].isbn");
288 |
289 | $this->assertEquals(['0-553-21311-3', '0-395-19395-8'], $result->data());
290 | }
291 |
292 | /**
293 | * $..*
294 | * All members of JSON structure
295 | */
296 | public function testRecursiveWithWildcard()
297 | {
298 | $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..*");
299 | $result = json_decode(json_encode($result), true);
300 |
301 | $this->assertEquals('Sayings of the Century', $result[0]['books'][0]['title']);
302 | $this->assertEquals(19.95, $result[26]);
303 | }
304 |
305 | /**
306 | * Tests direct key access.
307 | */
308 | public function testSimpleArrayAccess()
309 | {
310 | $result = (new JSONPath(['title' => 'test title']))->find('title');
311 |
312 | $this->assertEquals(['test title'], $result->data());
313 | }
314 |
315 | public function testFilteringOnNoneArrays()
316 | {
317 | $data = ['foo' => 'asdf'];
318 |
319 | $result = (new JSONPath($data))->find("$.foo.bar");
320 | $this->assertEquals([], $result->data());
321 | }
322 |
323 |
324 | public function testMagicMethods()
325 | {
326 | $fooClass = new JSONPathTestClass();
327 |
328 | $results = (new JSONPath($fooClass, JSONPath::ALLOW_MAGIC))->find('$.foo');
329 |
330 | $this->assertEquals(['bar'], $results->data());
331 | }
332 |
333 |
334 | public function testMatchWithComplexSquareBrackets()
335 | {
336 | $result = (new JSONPath($this->exampleDataExtra()))->find("$['http://www.w3.org/2000/01/rdf-schema#label'][?(@['@language']='en')]['@language']");
337 | $this->assertEquals(["en"], $result->data());
338 | }
339 |
340 | public function testQueryMatchWithRecursive()
341 | {
342 | $locations = $this->exampleDataLocations();
343 | $result = (new JSONPath($locations))->find("..[?(@.type == 'suburb')].name");
344 | $this->assertEquals(["Rosebank"], $result->data());
345 | }
346 |
347 | public function testFirst()
348 | {
349 | $result = (new JSONPath($this->exampleDataExtra()))->find("$['http://www.w3.org/2000/01/rdf-schema#label'].*");
350 |
351 | $this->assertEquals(["@language" => "en"], $result->first()->data());
352 | }
353 |
354 | public function testLast()
355 | {
356 | $result = (new JSONPath($this->exampleDataExtra()))->find("$['http://www.w3.org/2000/01/rdf-schema#label'].*");
357 | $this->assertEquals(["@language" => "de"], $result->last()->data());
358 | }
359 |
360 | public function testSlashesInIndex()
361 | {
362 | $result = (new JSONPath($this->exampleDataWithSlashes()))->find("$['mediatypes']['image/png']");
363 |
364 | $this->assertEquals(
365 | [
366 | "/core/img/filetypes/image.png",
367 | ],
368 | $result->data()
369 | );
370 | }
371 |
372 | public function testCyrillicText()
373 | {
374 | $result = (new JSONPath(["трололо" => 1]))->find("$['трололо']");
375 |
376 | $this->assertEquals([1], $result->data());
377 |
378 | $result = (new JSONPath(["трололо" => 1]))->find("$.трололо");
379 |
380 | $this->assertEquals([1], $result->data());
381 | }
382 |
383 | public function testOffsetUnset()
384 | {
385 | $data = [
386 | "route" => [
387 | ["name" => "A", "type" => "type of A"],
388 | ["name" => "B", "type" => "type of B"],
389 | ],
390 | ];
391 | $data = json_encode($data);
392 |
393 | $jsonIterator = new JSONPath(json_decode($data));
394 |
395 | /** @var JSONPath $route */
396 | $route = $jsonIterator->offsetGet('route');
397 |
398 | $route->offsetUnset(0);
399 |
400 | $first = $route->first();
401 |
402 | $this->assertEquals("B", $first['name']);
403 | }
404 |
405 |
406 | public function testFirstKey()
407 | {
408 | // Array test for array
409 | $jsonPath = new JSONPath(['a' => 'A', 'b', 'B']);
410 |
411 | $firstKey = $jsonPath->firstKey();
412 |
413 | $this->assertEquals('a', $firstKey);
414 |
415 | // Array test for object
416 | $jsonPath = new JSONPath((object)['a' => 'A', 'b', 'B']);
417 |
418 | $firstKey = $jsonPath->firstKey();
419 |
420 | $this->assertEquals('a', $firstKey);
421 | }
422 |
423 | public function testLastKey()
424 | {
425 | // Array test for array
426 | $jsonPath = new JSONPath(['a' => 'A', 'b' => 'B', 'c' => 'C']);
427 |
428 | $lastKey = $jsonPath->lastKey();
429 |
430 | $this->assertEquals('c', $lastKey);
431 |
432 | // Array test for object
433 | $jsonPath = new JSONPath((object)['a' => 'A', 'b' => 'B', 'c' => 'C']);
434 |
435 | $lastKey = $jsonPath->lastKey();
436 |
437 | $this->assertEquals('c', $lastKey);
438 | }
439 |
440 | /**
441 | * Test: ensure trailing comma is stripped during parsing
442 | */
443 | public function testTrailingComma()
444 | {
445 | $jsonPath = new JSONPath(json_decode('{"store":{"book":[{"category":"reference","author":"Nigel Rees","title":"Sayings of the Century","price":8.95},{"category":"fiction","author":"Evelyn Waugh","title":"Sword of Honour","price":12.99},{"category":"fiction","author":"Herman Melville","title":"Moby Dick","isbn":"0-553-21311-3","price":8.99},{"category":"fiction","author":"J. R. R. Tolkien","title":"The Lord of the Rings","isbn":"0-395-19395-8","price":22.99}],"bicycle":{"color":"red","price":19.95}},"expensive":10}'));
446 |
447 | $result = $jsonPath->find("$..book[0,1,2,]");
448 |
449 | $this->assertCount(3, $result);
450 | }
451 |
452 | /**
453 | * Test: ensure negative indexes return -n from last index
454 | */
455 | public function testNegativeIndex()
456 | {
457 | $jsonPath = new JSONPath(json_decode('{"store":{"book":[{"category":"reference","author":"Nigel Rees","title":"Sayings of the Century","price":8.95},{"category":"fiction","author":"Evelyn Waugh","title":"Sword of Honour","price":12.99},{"category":"fiction","author":"Herman Melville","title":"Moby Dick","isbn":"0-553-21311-3","price":8.99},{"category":"fiction","author":"J. R. R. Tolkien","title":"The Lord of the Rings","isbn":"0-395-19395-8","price":22.99}],"bicycle":{"color":"red","price":19.95}},"expensive":10}'));
458 |
459 | $result = $jsonPath->find("$..book[-2]");
460 |
461 | $this->assertEquals("Herman Melville", $result[0]['author']);
462 | }
463 |
464 | public function testQueryAccessWithNumericalIndexes()
465 | {
466 | $jsonPath = new JSONPath(json_decode('{
467 | "result": {
468 | "list": [
469 | {
470 | "time": 1477526400,
471 | "o": "11.51000"
472 | },
473 | {
474 | "time": 1477612800,
475 | "o": "11.49870"
476 | }
477 | ]
478 | }
479 | }'));
480 |
481 | $result = $jsonPath->find("$.result.list[?(@.o == \"11.51000\")]");
482 |
483 | $this->assertEquals("11.51000", $result[0]->o);
484 |
485 | $jsonPath = new JSONPath(json_decode('{
486 | "result": {
487 | "list": [
488 | [
489 | 1477526400,
490 | "11.51000"
491 | ],
492 | [
493 | 1477612800,
494 | "11.49870"
495 | ]
496 | ]
497 | }
498 | }'));
499 |
500 | $result = $jsonPath->find("$.result.list[?(@[1] == \"11.51000\")]");
501 |
502 | $this->assertEquals("11.51000", $result[0][1]);
503 |
504 | }
505 |
506 |
507 | public function exampleData($asArray = true)
508 | {
509 | $json = '
510 | {
511 | "store":{
512 | "books":[
513 | {
514 | "category":"reference",
515 | "author":"Nigel Rees",
516 | "title":"Sayings of the Century",
517 | "price":8.95
518 | },
519 | {
520 | "category":"fiction",
521 | "author":"Evelyn Waugh",
522 | "title":"Sword of Honour",
523 | "price":12.99
524 | },
525 | {
526 | "category":"fiction",
527 | "author":"Herman Melville",
528 | "title":"Moby Dick",
529 | "isbn":"0-553-21311-3",
530 | "price":8.99
531 | },
532 | {
533 | "category":"fiction",
534 | "author":"J. R. R. Tolkien",
535 | "title":"The Lord of the Rings",
536 | "isbn":"0-395-19395-8",
537 | "price":22.99
538 | }
539 | ],
540 | "bicycle":{
541 | "color":"red",
542 | "price":19.95
543 | }
544 | }
545 | }';
546 | return json_decode($json, $asArray);
547 | }
548 |
549 | public function exampleDataExtra($asArray = true)
550 | {
551 | $json = '
552 | {
553 | "http://www.w3.org/2000/01/rdf-schema#label":[
554 | {
555 | "@language":"en"
556 | },
557 | {
558 | "@language":"de"
559 | }
560 | ]
561 | }
562 | ';
563 |
564 | return json_decode($json, $asArray);
565 | }
566 |
567 |
568 | public function exampleDataLocations($asArray = true)
569 | {
570 | $json = '
571 | {
572 | "name": "Gauteng",
573 | "type": "province",
574 | "child": {
575 | "name": "Johannesburg",
576 | "type": "city",
577 | "child": {
578 | "name": "Rosebank",
579 | "type": "suburb"
580 | }
581 | }
582 | }
583 | ';
584 |
585 | return json_decode($json, $asArray);
586 | }
587 |
588 |
589 | public function exampleDataWithSlashes($asArray = true)
590 | {
591 | $json = '
592 | {
593 | "features": [],
594 | "mediatypes": {
595 | "image/png": "/core/img/filetypes/image.png",
596 | "image/jpeg": "/core/img/filetypes/image.png",
597 | "image/gif": "/core/img/filetypes/image.png",
598 | "application/postscript": "/core/img/filetypes/image-vector.png"
599 | }
600 | }
601 | ';
602 |
603 | return json_decode($json, $asArray);
604 | }
605 |
606 | public function exampleDataWithSimpleIntegers($asArray = true)
607 | {
608 | $json = '
609 | {
610 | "features": [{"name": "foo", "value": 1},{"name": "bar", "value": 2},{"name": "baz", "value": 1}]
611 | }
612 | ';
613 |
614 | return json_decode($json, $asArray);
615 | }
616 |
617 |
618 | }
619 |
620 | class JSONPathTestClass
621 | {
622 | protected $attributes = [
623 | 'foo' => 'bar',
624 | ];
625 |
626 | public function __get($key)
627 | {
628 | return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
629 | }
630 | }
631 |
--------------------------------------------------------------------------------