├── .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 [![Build Status](https://travis-ci.org/FlowCommunications/JSONPath.svg?branch=master)](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 | --------------------------------------------------------------------------------