├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Exception.php ├── InvalidFieldTypeException.php ├── JsonDiff.php ├── JsonHash.php ├── JsonMergePatch.php ├── JsonPatch.php ├── JsonPatch ├── Add.php ├── Copy.php ├── Move.php ├── OpPath.php ├── OpPathFrom.php ├── OpPathValue.php ├── Remove.php ├── Replace.php └── Test.php ├── JsonPointer.php ├── JsonPointerException.php ├── JsonValueReplace.php ├── MissingFieldException.php ├── ModifiedPathDiff.php ├── PatchTestOperationFailedException.php ├── PathException.php └── UnknownOperationException.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [3.10.4] - 2022-11-09 8 | 9 | ### Fixed 10 | - Handling of non-string `op` values, [#63](https://github.com/swaggest/json-diff/pull/63). 11 | 12 | ## [3.10.3] - 2022-11-08 13 | 14 | ### Added 15 | - Option to skip paths, [#39](https://github.com/swaggest/json-diff/pull/39). 16 | 17 | ## [3.10.2] - 2022-11-08 18 | 19 | ### Added 20 | - Exceptions improved, [#60](https://github.com/swaggest/json-diff/pull/60). 21 | 22 | ## [3.10.1] - 2022-10-24 23 | 24 | ### Added 25 | - Exception type hints improved, [#59](https://github.com/swaggest/json-diff/pull/59). 26 | 27 | ## [3.10.0] - 2022-10-21 28 | 29 | ### Added 30 | - More specific exception types added for failed test operation, [#57](https://github.com/swaggest/json-diff/pull/57). 31 | 32 | 33 | ## [3.9.0] - 2022-08-29 34 | 35 | ### Added 36 | - Specific exception type `PatchTestOperationFailedException` for failed test operation. 37 | 38 | ## [3.8.3] - 2021-09-25 39 | 40 | ### Fixed 41 | - Redundant operations in array patches that strip first element. 42 | - XOR hash collision for properties having equal parts. 43 | 44 | ### Added 45 | - Rearrange indexing by non-scalar properties that contain object, using XOR hash. 46 | 47 | ## [3.8.2] - 2021-09-17 48 | 49 | ### Fixed 50 | - PHP 8.1 Deprecation notices with missing returns. 51 | 52 | ## [3.8.1] - 2020-09-25 53 | 54 | ### Fixed 55 | - Rearrangement of equal array items is corrupting data by redundant replaces. 56 | 57 | ## [3.8.0] - 2020-09-25 58 | 59 | ### Added 60 | - Rearrangement of equal items for non-homogeneous arrays with `JsonDiff::REARRANGE_ARRAYS` option. 61 | 62 | ## [3.7.5] - 2020-05-26 63 | 64 | ### Fixed 65 | - Accidental array to associative array conversion ([#31](https://github.com/swaggest/json-diff/issues/31)). 66 | 67 | ## [3.7.4] - 2020-01-26 68 | 69 | ### Fixed 70 | - PHP version check for empty property name support. 71 | 72 | ## [3.7.3] - 2020-01-24 73 | 74 | ### Fixed 75 | - Merge patch was not replacing partially different arrays. 76 | 77 | ## [3.7.2] - 2019-10-23 78 | 79 | ### Added 80 | - Change log. 81 | 82 | ### Fixed 83 | - Few irrelevant files not mentioned in `.gitattributes`. 84 | 85 | ## [3.7.1] - 2019-09-26 86 | 87 | ### Added 88 | - Benchmarks to CI. 89 | 90 | ### Fixed 91 | - Unstable array rearrange order. 92 | 93 | ## [3.7.0] - 2019-04-25 94 | 95 | ### Added 96 | - `getModifiedDiff()` and `COLLECT_MODIFIED_DIFF` option to return paths with original and new values. 97 | 98 | ## [3.6.0] - 2019-04-24 99 | 100 | ### Added 101 | - Compatibility option to `TOLERATE_ASSOCIATIVE_ARRAYS` that mimic JSON objects. 102 | 103 | [3.10.4]: https://github.com/swaggest/json-diff/compare/v3.10.3...v3.10.4 104 | [3.10.3]: https://github.com/swaggest/json-diff/compare/v3.10.2...v3.10.3 105 | [3.10.2]: https://github.com/swaggest/json-diff/compare/v3.10.1...v3.10.2 106 | [3.10.1]: https://github.com/swaggest/json-diff/compare/v3.10.0...v3.10.1 107 | [3.10.0]: https://github.com/swaggest/json-diff/compare/v3.9.0...v3.10.0 108 | [3.9.0]: https://github.com/swaggest/json-diff/compare/v3.8.3...v3.9.0 109 | [3.8.3]: https://github.com/swaggest/json-diff/compare/v3.8.2...v3.8.3 110 | [3.8.2]: https://github.com/swaggest/json-diff/compare/v3.8.1...v3.8.2 111 | [3.8.1]: https://github.com/swaggest/json-diff/compare/v3.8.0...v3.8.1 112 | [3.8.0]: https://github.com/swaggest/json-diff/compare/v3.7.5...v3.8.0 113 | [3.7.5]: https://github.com/swaggest/json-diff/compare/v3.7.4...v3.7.5 114 | [3.7.4]: https://github.com/swaggest/json-diff/compare/v3.7.3...v3.7.4 115 | [3.7.3]: https://github.com/swaggest/json-diff/compare/v3.7.2...v3.7.3 116 | [3.7.2]: https://github.com/swaggest/json-diff/compare/v3.7.1...v3.7.2 117 | [3.7.1]: https://github.com/swaggest/json-diff/compare/v3.7.0...v3.7.1 118 | [3.7.0]: https://github.com/swaggest/json-diff/compare/v3.6.0...v3.7.0 119 | [3.6.0]: https://github.com/swaggest/json-diff/compare/v3.5.1...v3.6.0 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 swaggest 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 | # JSON diff/rearrange/patch/pointer library for PHP 2 | 3 | A PHP implementation for finding unordered diff between two `JSON` documents. 4 | 5 | [![Build Status](https://travis-ci.org/swaggest/json-diff.svg?branch=master)](https://travis-ci.org/swaggest/json-diff) 6 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/swaggest/json-diff/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/swaggest/json-diff/?branch=master) 7 | [![Code Climate](https://codeclimate.com/github/swaggest/json-diff/badges/gpa.svg)](https://codeclimate.com/github/swaggest/json-diff) 8 | [![Code Coverage](https://scrutinizer-ci.com/g/swaggest/json-diff/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/swaggest/json-diff/code-structure/master/code-coverage) 9 | [![time tracker](https://wakatime.com/badge/github/swaggest/json-diff.svg)](https://wakatime.com/badge/github/swaggest/json-diff) 10 | 11 | 12 | ## Purpose 13 | 14 | * To simplify changes review between two `JSON` files you can use a standard `diff` tool on rearranged pretty-printed `JSON`. 15 | * To detect breaking changes by analyzing removals and changes from original `JSON`. 16 | * To keep original order of object sets (for example `swagger.json` [parameters](https://swagger.io/docs/specification/describing-parameters/) list). 17 | * To [make](#getpatch) and [apply](#jsonpatch) JSON Patches, specified in [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) from the IETF. 18 | * To [make](#getmergepatch) and [apply](#jsonmergepatch) JSON Merge Patches, specified in [RFC 7386](https://datatracker.ietf.org/doc/html/rfc7386) from the IETF. 19 | * To retrieve and modify data by [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901). 20 | * To recursively replace by JSON value. 21 | 22 | ## Installation 23 | 24 | ### Library 25 | 26 | ```bash 27 | git clone https://github.com/swaggest/json-diff.git 28 | ``` 29 | 30 | ### Composer 31 | 32 | [Install PHP Composer](https://getcomposer.org/doc/00-intro.md) 33 | 34 | ```bash 35 | composer require swaggest/json-diff 36 | ``` 37 | 38 | ## Library usage 39 | 40 | ### `JsonDiff` 41 | 42 | Create `JsonDiff` object from two values (`original` and `new`). 43 | 44 | ```php 45 | $r = new JsonDiff(json_decode($originalJson), json_decode($newJson)); 46 | ``` 47 | 48 | On construction `JsonDiff` will build `rearranged` value of `new` recursively keeping `original` keys order where possible. 49 | Keys that are missing in `original` will be appended to the end of `rearranged` value in same order they had in `new` value. 50 | 51 | If two values are arrays of objects, `JsonDiff` will try to find a common unique field in those objects and use it as criteria for rearranging. 52 | You can enable this behaviour with `JsonDiff::REARRANGE_ARRAYS` option: 53 | ```php 54 | $r = new JsonDiff( 55 | json_decode($originalJson), 56 | json_decode($newJson), 57 | JsonDiff::REARRANGE_ARRAYS 58 | ); 59 | ``` 60 | 61 | Available options: 62 | * `REARRANGE_ARRAYS` is an option to enable [arrays rearrangement](#arrays-rearrangement) to minimize the difference. 63 | * `STOP_ON_DIFF` is an option to improve performance by stopping comparison when a difference is found. 64 | * `JSON_URI_FRAGMENT_ID` is an option to use URI Fragment Identifier Representation (example: "#/c%25d"). If not set default JSON String Representation (example: "/c%d"). 65 | * `SKIP_JSON_PATCH` is an option to improve performance by not building JsonPatch for this diff. 66 | * `SKIP_JSON_MERGE_PATCH` is an option to improve performance by not building JSON Merge Patch value for this diff. 67 | * `TOLERATE_ASSOCIATIVE_ARRAYS` is an option to allow associative arrays to mimic JSON objects (not recommended). 68 | * `COLLECT_MODIFIED_DIFF` is an option to enable [getModifiedDiff](#getmodifieddiff). 69 | 70 | Options can be combined, e.g. `JsonDiff::REARRANGE_ARRAYS + JsonDiff::STOP_ON_DIFF`. 71 | 72 | #### `getDiffCnt` 73 | Returns total number of differences 74 | 75 | #### `getPatch` 76 | Returns [`JsonPatch`](#jsonpatch) of difference 77 | 78 | #### `getMergePatch` 79 | Returns [JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7386) value of difference 80 | 81 | #### `getRearranged` 82 | Returns new value, rearranged with original order. 83 | 84 | #### `getRemoved` 85 | Returns removals as partial value of original. 86 | 87 | #### `getRemovedPaths` 88 | Returns list of `JSON` paths that were removed from original. 89 | 90 | #### `getRemovedCnt` 91 | Returns number of removals. 92 | 93 | #### `getAdded` 94 | Returns additions as partial value of new. 95 | 96 | #### `getAddedPaths` 97 | Returns list of `JSON` paths that were added to new. 98 | 99 | #### `getAddedCnt` 100 | Returns number of additions. 101 | 102 | #### `getModifiedOriginal` 103 | Returns modifications as partial value of original. 104 | 105 | #### `getModifiedNew` 106 | Returns modifications as partial value of new. 107 | 108 | #### `getModifiedDiff` 109 | Returns list of [`ModifiedPathDiff`](src/ModifiedPathDiff.php) containing paths with original and new values. 110 | 111 | Not collected by default, requires `JsonDiff::COLLECT_MODIFIED_DIFF` option. 112 | 113 | #### `getModifiedPaths` 114 | Returns list of `JSON` paths that were modified from original to new. 115 | 116 | #### `getModifiedCnt` 117 | Returns number of modifications. 118 | 119 | ### `JsonPatch` 120 | 121 | #### `import` 122 | Creates `JsonPatch` instance from `JSON`-decoded data. 123 | 124 | #### `export` 125 | Creates patch data from `JsonPatch` object. 126 | 127 | #### `op` 128 | Adds operation to `JsonPatch`. 129 | 130 | #### `apply` 131 | Applies patch to `JSON`-decoded data. 132 | 133 | #### `setFlags` 134 | Alters default behavior. 135 | 136 | Available flags: 137 | 138 | * `JsonPatch::STRICT_MODE` Disallow converting empty array to object for key creation. 139 | * `JsonPatch::TOLERATE_ASSOCIATIVE_ARRAYS` Allow associative arrays to mimic JSON objects (not recommended). 140 | 141 | ### `JsonPointer` 142 | 143 | #### `escapeSegment` 144 | Escapes path segment. 145 | 146 | #### `splitPath` 147 | Creates array of unescaped segments from `JSON Pointer` string. 148 | 149 | #### `buildPath` 150 | Creates `JSON Pointer` string from array of unescaped segments. 151 | 152 | #### `add` 153 | Adds value to data at path specified by segments. 154 | 155 | #### `get` 156 | Gets value from data at path specified by segments. 157 | 158 | #### `getByPointer` 159 | Gets value from data at path specified `JSON Pointer` string. 160 | 161 | #### `remove` 162 | Removes value from data at path specified by segments. 163 | 164 | ### `JsonMergePatch` 165 | 166 | #### `apply` 167 | Applies patch to `JSON`-decoded data. 168 | 169 | ### `JsonValueReplace` 170 | 171 | #### `process` 172 | Recursively replaces all nodes equal to `search` value with `replace` value. 173 | 174 | ## Example 175 | 176 | ```php 177 | $originalJson = <<<'JSON' 178 | { 179 | "key1": [4, 1, 2, 3], 180 | "key2": 2, 181 | "key3": { 182 | "sub0": 0, 183 | "sub1": "a", 184 | "sub2": "b" 185 | }, 186 | "key4": [ 187 | {"a":1, "b":true, "subs": [{"s":1}, {"s":2}, {"s":3}]}, {"a":2, "b":false}, {"a":3} 188 | ] 189 | } 190 | JSON; 191 | 192 | $newJson = <<<'JSON' 193 | { 194 | "key5": "wat", 195 | "key1": [5, 1, 2, 3], 196 | "key4": [ 197 | {"c":false, "a":2}, {"a":1, "b":true, "subs": [{"s":3, "add": true}, {"s":2}, {"s":1}]}, {"c":1, "a":3} 198 | ], 199 | "key3": { 200 | "sub3": 0, 201 | "sub2": false, 202 | "sub1": "c" 203 | } 204 | } 205 | JSON; 206 | 207 | $patchJson = <<<'JSON' 208 | [ 209 | {"value":4,"op":"test","path":"/key1/0"}, 210 | {"value":5,"op":"replace","path":"/key1/0"}, 211 | 212 | {"op":"remove","path":"/key2"}, 213 | 214 | {"op":"remove","path":"/key3/sub0"}, 215 | 216 | {"value":"a","op":"test","path":"/key3/sub1"}, 217 | {"value":"c","op":"replace","path":"/key3/sub1"}, 218 | 219 | {"value":"b","op":"test","path":"/key3/sub2"}, 220 | {"value":false,"op":"replace","path":"/key3/sub2"}, 221 | 222 | {"value":0,"op":"add","path":"/key3/sub3"}, 223 | 224 | {"value":true,"op":"add","path":"/key4/0/subs/2/add"}, 225 | 226 | {"op":"remove","path":"/key4/1/b"}, 227 | 228 | {"value":false,"op":"add","path":"/key4/1/c"}, 229 | 230 | {"value":1,"op":"add","path":"/key4/2/c"}, 231 | 232 | {"value":"wat","op":"add","path":"/key5"} 233 | ] 234 | JSON; 235 | 236 | $diff = new JsonDiff(json_decode($originalJson), json_decode($newJson), JsonDiff::REARRANGE_ARRAYS); 237 | $this->assertEquals(json_decode($patchJson), $diff->getPatch()->jsonSerialize()); 238 | 239 | $original = json_decode($originalJson); 240 | $patch = JsonPatch::import(json_decode($patchJson)); 241 | $patch->apply($original); 242 | $this->assertEquals($diff->getRearranged(), $original); 243 | ``` 244 | 245 | ## PHP Classes as JSON objects 246 | 247 | Due to magical methods and other restrictions PHP classes can not be reliably mapped to/from JSON objects. 248 | There is support for objects of PHP classes in `JsonPointer` with limitations: 249 | * `null` is equal to non-existent 250 | 251 | ## Arrays Rearrangement 252 | 253 | When `JsonDiff::REARRANGE_ARRAYS` option is enabled, array items are ordered to match the original array. 254 | 255 | If arrays contain homogenous objects, and those objects have a common property with unique values, array is 256 | ordered to match placement of items with same value of such property in the original array. 257 | 258 | Example: 259 | original 260 | ```json 261 | [{"name": "Alex", "height": 180},{"name": "Joe", "height": 179},{"name": "Jane", "height": 165}] 262 | ``` 263 | vs new 264 | ```json 265 | [{"name": "Joe", "height": 179},{"name": "Jane", "height": 168},{"name": "Alex", "height": 180}] 266 | ``` 267 | would produce a patch: 268 | ```json 269 | [{"value":165,"op":"test","path":"/2/height"},{"value":168,"op":"replace","path":"/2/height"}] 270 | ``` 271 | 272 | If qualifying indexing property is not found, rearrangement is done based on items equality. 273 | 274 | Example: 275 | original 276 | ```json 277 | {"data": [{"A": 1, "C": [1, 2, 3]}, {"B": 2}]} 278 | ``` 279 | vs new 280 | ```json 281 | {"data": [{"B": 2}, {"A": 1, "C": [3, 2, 1]}]} 282 | ``` 283 | would produce no difference. 284 | 285 | ## CLI tool 286 | 287 | Moved to [`swaggest/json-cli`](https://github.com/swaggest/json-cli) 288 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swaggest/json-diff", 3 | "description": "JSON diff/rearrange/patch/pointer library for PHP", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Viacheslav Poturaev", 9 | "email": "vearutop@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "ext-json": "*", 14 | "php": ">=7.1" 15 | }, 16 | "require-dev": { 17 | "phperf/phpunit": "4.8.37" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Swaggest\\JsonDiff\\": "src/" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "Swaggest\\JsonDiff\\Tests\\": "tests/src" 27 | } 28 | }, 29 | "config": { 30 | "platform": { 31 | "php": "7.1.33" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | field = $field; 39 | $this->expectedType = $expectedType; 40 | $this->operation = $operation; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getField() 47 | { 48 | return $this->field; 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function getExpectedType() 55 | { 56 | return $this->expectedType; 57 | } 58 | 59 | /** 60 | * @return OpPath|object 61 | */ 62 | public function getOperation() 63 | { 64 | return $this->operation; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/JsonDiff.php: -------------------------------------------------------------------------------- 1 | jsonPatch = new JsonPatch(); 105 | } 106 | 107 | $this->options = $options; 108 | 109 | $this->skipPaths = $skipPaths; 110 | 111 | if ($options & self::JSON_URI_FRAGMENT_ID) { 112 | $this->path = '#'; 113 | } 114 | 115 | $this->rearranged = $this->process($original, $new); 116 | if (($new !== null) && $this->merge === null) { 117 | $this->merge = new \stdClass(); 118 | } 119 | } 120 | 121 | /** 122 | * Returns total number of differences 123 | * @return int 124 | */ 125 | public function getDiffCnt() 126 | { 127 | return $this->addedCnt + $this->modifiedCnt + $this->removedCnt; 128 | } 129 | 130 | /** 131 | * Returns removals as partial value of original. 132 | * @return mixed 133 | */ 134 | public function getRemoved() 135 | { 136 | return $this->removed; 137 | } 138 | 139 | /** 140 | * Returns list of `JSON` paths that were removed from original. 141 | * @return array 142 | */ 143 | public function getRemovedPaths() 144 | { 145 | return $this->removedPaths; 146 | } 147 | 148 | /** 149 | * Returns number of removals. 150 | * @return int 151 | */ 152 | public function getRemovedCnt() 153 | { 154 | return $this->removedCnt; 155 | } 156 | 157 | /** 158 | * Returns additions as partial value of new. 159 | * @return mixed 160 | */ 161 | public function getAdded() 162 | { 163 | return $this->added; 164 | } 165 | 166 | /** 167 | * Returns number of additions. 168 | * @return int 169 | */ 170 | public function getAddedCnt() 171 | { 172 | return $this->addedCnt; 173 | } 174 | 175 | /** 176 | * Returns list of `JSON` paths that were added to new. 177 | * @return array 178 | */ 179 | public function getAddedPaths() 180 | { 181 | return $this->addedPaths; 182 | } 183 | 184 | /** 185 | * Returns changes as partial value of original. 186 | * @return mixed 187 | */ 188 | public function getModifiedOriginal() 189 | { 190 | return $this->modifiedOriginal; 191 | } 192 | 193 | /** 194 | * Returns changes as partial value of new. 195 | * @return mixed 196 | */ 197 | public function getModifiedNew() 198 | { 199 | return $this->modifiedNew; 200 | } 201 | 202 | /** 203 | * Returns number of changes. 204 | * @return int 205 | */ 206 | public function getModifiedCnt() 207 | { 208 | return $this->modifiedCnt; 209 | } 210 | 211 | /** 212 | * Returns list of `JSON` paths that were changed from original to new. 213 | * @return array 214 | */ 215 | public function getModifiedPaths() 216 | { 217 | return $this->modifiedPaths; 218 | } 219 | 220 | /** 221 | * Returns list of paths with original and new values. 222 | * @return ModifiedPathDiff[] 223 | */ 224 | public function getModifiedDiff() 225 | { 226 | return $this->modifiedDiff; 227 | } 228 | 229 | /** 230 | * Returns new value, rearranged with original order. 231 | * @return array|object 232 | */ 233 | public function getRearranged() 234 | { 235 | return $this->rearranged; 236 | } 237 | 238 | /** 239 | * Returns JsonPatch of difference 240 | * @return JsonPatch 241 | */ 242 | public function getPatch() 243 | { 244 | return $this->jsonPatch; 245 | } 246 | 247 | /** 248 | * Returns JSON Merge Patch value of difference 249 | */ 250 | public function getMergePatch() 251 | { 252 | return $this->merge; 253 | 254 | } 255 | 256 | 257 | /** 258 | * @param mixed $original 259 | * @param mixed $new 260 | * @return array|null|object|\stdClass 261 | * @throws Exception 262 | */ 263 | private function process($original, $new) 264 | { 265 | $merge = !($this->options & self::SKIP_JSON_MERGE_PATCH); 266 | 267 | $addTestOps = !($this->options & self::SKIP_TEST_OPS); 268 | 269 | if ($this->options & self::TOLERATE_ASSOCIATIVE_ARRAYS) { 270 | if (is_array($original) && !empty($original) && !array_key_exists(0, $original)) { 271 | $original = (object)$original; 272 | } 273 | 274 | if (is_array($new) && !empty($new) && !array_key_exists(0, $new)) { 275 | $new = (object)$new; 276 | } 277 | } 278 | 279 | if ( 280 | (!$original instanceof \stdClass && !is_array($original)) 281 | || (!$new instanceof \stdClass && !is_array($new)) 282 | ) { 283 | if ($original !== $new && !in_array($this->path, $this->skipPaths)) { 284 | $this->modifiedCnt++; 285 | if ($this->options & self::STOP_ON_DIFF) { 286 | return null; 287 | } 288 | $this->modifiedPaths [] = $this->path; 289 | 290 | if ($this->jsonPatch !== null) { 291 | if ($addTestOps) { 292 | $this->jsonPatch->op(new Test($this->path, $original)); 293 | } 294 | $this->jsonPatch->op(new Replace($this->path, $new)); 295 | } 296 | 297 | JsonPointer::add($this->modifiedOriginal, $this->pathItems, $original); 298 | JsonPointer::add($this->modifiedNew, $this->pathItems, $new); 299 | 300 | if ($merge) { 301 | JsonPointer::add($this->merge, $this->pathItems, $new, JsonPointer::RECURSIVE_KEY_CREATION); 302 | } 303 | 304 | if ($this->options & self::COLLECT_MODIFIED_DIFF) { 305 | $this->modifiedDiff[] = new ModifiedPathDiff($this->path, $original, $new); 306 | } 307 | } 308 | return $new; 309 | } 310 | 311 | if ( 312 | ($this->options & self::REARRANGE_ARRAYS) 313 | && is_array($original) && is_array($new) 314 | ) { 315 | $new = $this->rearrangeArray($original, $new); 316 | } 317 | 318 | $newArray = $new instanceof \stdClass ? get_object_vars($new) : $new; 319 | $newOrdered = array(); 320 | 321 | $originalKeys = $original instanceof \stdClass ? get_object_vars($original) : $original; 322 | $isArray = is_array($original); 323 | $removedOffset = 0; 324 | 325 | if ($merge && is_array($new) && !is_array($original)) { 326 | $merge = false; 327 | JsonPointer::add($this->merge, $this->pathItems, $new); 328 | } elseif ($merge && $new instanceof \stdClass && !$original instanceof \stdClass) { 329 | $merge = false; 330 | JsonPointer::add($this->merge, $this->pathItems, $new); 331 | } 332 | 333 | $isUriFragment = (bool)($this->options & self::JSON_URI_FRAGMENT_ID); 334 | $diffCnt = $this->addedCnt + $this->modifiedCnt + $this->removedCnt; 335 | foreach ($originalKeys as $key => $originalValue) { 336 | if ($this->options & self::STOP_ON_DIFF) { 337 | if ($this->modifiedCnt || $this->addedCnt || $this->removedCnt) { 338 | return null; 339 | } 340 | } 341 | 342 | $path = $this->path; 343 | $pathItems = $this->pathItems; 344 | $actualKey = $key; 345 | if ($isArray && is_int($actualKey)) { 346 | $actualKey -= $removedOffset; 347 | } 348 | $this->path .= '/' . JsonPointer::escapeSegment((string)$actualKey, $isUriFragment); 349 | $this->pathItems[] = $actualKey; 350 | 351 | if (array_key_exists($key, $newArray)) { 352 | $newOrdered[$key] = $this->process($originalValue, $newArray[$key]); 353 | unset($newArray[$key]); 354 | } else { 355 | $this->removedCnt++; 356 | if ($this->options & self::STOP_ON_DIFF) { 357 | return null; 358 | } 359 | $this->removedPaths [] = $this->path; 360 | if ($isArray) { 361 | $removedOffset++; 362 | } 363 | 364 | if ($this->jsonPatch !== null) { 365 | $this->jsonPatch->op(new Remove($this->path)); 366 | } 367 | 368 | JsonPointer::add($this->removed, $this->pathItems, $originalValue); 369 | if ($merge) { 370 | JsonPointer::add($this->merge, $this->pathItems, null); 371 | } 372 | 373 | } 374 | $this->path = $path; 375 | $this->pathItems = $pathItems; 376 | } 377 | 378 | if ($merge && $isArray && $this->addedCnt + $this->modifiedCnt + $this->removedCnt > $diffCnt) { 379 | JsonPointer::add($this->merge, $this->pathItems, $new); 380 | } 381 | 382 | // additions 383 | foreach ($newArray as $key => $value) { 384 | $this->addedCnt++; 385 | if ($this->options & self::STOP_ON_DIFF) { 386 | return null; 387 | } 388 | $newOrdered[$key] = $value; 389 | $path = $this->path . '/' . JsonPointer::escapeSegment($key, $isUriFragment); 390 | $pathItems = $this->pathItems; 391 | $pathItems[] = $key; 392 | JsonPointer::add($this->added, $pathItems, $value); 393 | if ($merge) { 394 | JsonPointer::add($this->merge, $pathItems, $value); 395 | } 396 | 397 | $this->addedPaths [] = $path; 398 | 399 | if ($this->jsonPatch !== null) { 400 | $this->jsonPatch->op(new Add($path, $value)); 401 | } 402 | 403 | } 404 | 405 | return is_array($new) ? $newOrdered : (object)$newOrdered; 406 | } 407 | 408 | /** 409 | * @param array $original 410 | * @param array $new 411 | * @return array 412 | */ 413 | private function rearrangeArray(array $original, array $new) 414 | { 415 | $first = reset($original); 416 | if (!$first instanceof \stdClass) { 417 | return $this->rearrangeEqualItems($original, $new); 418 | } 419 | 420 | $uniqueKey = false; 421 | $uniqueIdx = array(); 422 | 423 | // find unique key for all items 424 | /** @var mixed[string] $f */ 425 | $f = get_object_vars($first); 426 | foreach ($f as $key => $value) { 427 | if (is_array($value)) { 428 | continue; 429 | } 430 | 431 | $keyIsUnique = true; 432 | $uniqueIdx = array(); 433 | foreach ($original as $idx => $item) { 434 | if (!$item instanceof \stdClass) { 435 | return $new; 436 | } 437 | if (!isset($item->$key)) { 438 | $keyIsUnique = false; 439 | break; 440 | } 441 | $value = $item->$key; 442 | if (is_array($value)) { 443 | $keyIsUnique = false; 444 | break; 445 | } 446 | 447 | if ($value instanceof \stdClass) { 448 | if ($this->jsonHash === null) { 449 | $this->jsonHash = new JsonHash($this->options); 450 | } 451 | 452 | $value = $this->jsonHash->xorHash($value); 453 | } 454 | 455 | if (isset($uniqueIdx[$value])) { 456 | $keyIsUnique = false; 457 | break; 458 | } 459 | $uniqueIdx[$value] = $idx; 460 | } 461 | 462 | if ($keyIsUnique) { 463 | $uniqueKey = $key; 464 | break; 465 | } 466 | } 467 | 468 | if (!$uniqueKey) { 469 | return $this->rearrangeEqualItems($original, $new); 470 | } 471 | 472 | $newRearranged = []; 473 | $changedItems = []; 474 | 475 | foreach ($new as $item) { 476 | if (!$item instanceof \stdClass) { 477 | return $new; 478 | } 479 | 480 | if (!property_exists($item, $uniqueKey)) { 481 | return $new; 482 | } 483 | 484 | $value = $item->$uniqueKey; 485 | 486 | if (is_array($value)) { 487 | return $new; 488 | } 489 | 490 | if ($value instanceof \stdClass) { 491 | if ($this->jsonHash === null) { 492 | $this->jsonHash = new JsonHash($this->options); 493 | } 494 | 495 | $value = $this->jsonHash->xorHash($value); 496 | } 497 | 498 | 499 | if (isset($uniqueIdx[$value])) { 500 | $idx = $uniqueIdx[$value]; 501 | // Abandon rearrangement if key is not unique in new array. 502 | if (isset($newRearranged[$idx])) { 503 | return $new; 504 | } 505 | 506 | $newRearranged[$idx] = $item; 507 | } else { 508 | $changedItems[] = $item; 509 | } 510 | 511 | $newIdx[$value] = $item; 512 | } 513 | 514 | $idx = 0; 515 | foreach ($changedItems as $item) { 516 | while (array_key_exists($idx, $newRearranged)) { 517 | $idx++; 518 | } 519 | $newRearranged[$idx] = $item; 520 | } 521 | 522 | ksort($newRearranged); 523 | return $newRearranged; 524 | } 525 | 526 | private function rearrangeEqualItems(array $original, array $new) 527 | { 528 | if ($this->jsonHash === null) { 529 | $this->jsonHash = new JsonHash($this->options); 530 | } 531 | 532 | $origIdx = []; 533 | foreach ($original as $i => $item) { 534 | $hash = $this->jsonHash->xorHash($item); 535 | $origIdx[$hash][] = $i; 536 | } 537 | 538 | $newIdx = []; 539 | foreach ($new as $i => $item) { 540 | $hash = $this->jsonHash->xorHash($item); 541 | $newIdx[$i] = $hash; 542 | } 543 | 544 | $newRearranged = []; 545 | $changedItems = []; 546 | foreach ($newIdx as $i => $hash) { 547 | if (!empty($origIdx[$hash])) { 548 | $j = array_shift($origIdx[$hash]); 549 | 550 | $newRearranged[$j] = $new[$i]; 551 | } else { 552 | $changedItems []= $new[$i]; 553 | } 554 | 555 | } 556 | 557 | $idx = 0; 558 | foreach ($changedItems as $item) { 559 | while (array_key_exists($idx, $newRearranged)) { 560 | $idx++; 561 | } 562 | $newRearranged[$idx] = $item; 563 | } 564 | 565 | ksort($newRearranged); 566 | 567 | return $newRearranged; 568 | } 569 | } 570 | -------------------------------------------------------------------------------- /src/JsonHash.php: -------------------------------------------------------------------------------- 1 | options = $options; 12 | } 13 | 14 | /** 15 | * @param mixed $data 16 | * @param string $path 17 | * @return string 18 | */ 19 | public function xorHash($data, $path = '') 20 | { 21 | $xorHash = ''; 22 | 23 | if (!$data instanceof \stdClass && !is_array($data)) { 24 | $s = $path . (string)$data; 25 | if (strlen($xorHash) < strlen($s)) { 26 | $xorHash = str_pad($xorHash, strlen($s)); 27 | } 28 | $xorHash ^= $s; 29 | 30 | return $xorHash; 31 | } 32 | 33 | if ($this->options & JsonDiff::TOLERATE_ASSOCIATIVE_ARRAYS) { 34 | if (is_array($data) && !empty($data) && !array_key_exists(0, $data)) { 35 | $data = (object)$data; 36 | } 37 | } 38 | 39 | if (is_array($data)) { 40 | if ($this->options & JsonDiff::REARRANGE_ARRAYS) { 41 | foreach ($data as $key => $item) { 42 | $itemPath = $path . '/' . $key; 43 | $itemHash = $path . $this->xorHash($item, $itemPath); 44 | if (strlen($xorHash) < strlen($itemHash)) { 45 | $xorHash = str_pad($xorHash, strlen($itemHash)); 46 | } 47 | $xorHash ^= $itemHash; 48 | } 49 | } else { 50 | foreach ($data as $key => $item) { 51 | $itemPath = $path . '/' . $key; 52 | $itemHash = md5($itemPath . $this->xorHash($item, $itemPath), true); 53 | if (strlen($xorHash) < strlen($itemHash)) { 54 | $xorHash = str_pad($xorHash, strlen($itemHash)); 55 | } 56 | $xorHash ^= $itemHash; 57 | } 58 | } 59 | 60 | return $xorHash; 61 | } 62 | 63 | $dataKeys = get_object_vars($data); 64 | foreach ($dataKeys as $key => $value) { 65 | $propertyPath = $path . '/' . 66 | JsonPointer::escapeSegment($key, (bool)($this->options & JsonDiff::JSON_URI_FRAGMENT_ID)); 67 | $propertyHash = $propertyPath . md5($key, true) . $this->xorHash($value, $propertyPath); 68 | if (strlen($xorHash) < strlen($propertyHash)) { 69 | $xorHash = str_pad($xorHash, strlen($propertyHash)); 70 | } 71 | $xorHash ^= $propertyHash; 72 | } 73 | 74 | return $xorHash; 75 | } 76 | } -------------------------------------------------------------------------------- /src/JsonMergePatch.php: -------------------------------------------------------------------------------- 1 | $val) { 14 | if ($val === null) { 15 | unset($original->$key); 16 | } else { 17 | if (!is_object($original)) { 18 | $original = new \stdClass(); 19 | } 20 | $branch = &$original->$key; 21 | if (null === $branch) { 22 | $branch = new \stdClass(); 23 | } 24 | self::apply($branch, $val); 25 | } 26 | } 27 | } else { 28 | $original = $patch; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/JsonPatch.php: -------------------------------------------------------------------------------- 1 | flags = $options; 43 | return $this; 44 | } 45 | 46 | /** @var OpPath[] */ 47 | private $operations = array(); 48 | 49 | /** 50 | * @param array $data 51 | * @return JsonPatch 52 | * @throws Exception 53 | */ 54 | public static function import(array $data) 55 | { 56 | $result = new JsonPatch(); 57 | foreach ($data as $operation) { 58 | /** @var OpPath|OpPathValue|OpPathFrom|array $operation */ 59 | if (is_array($operation)) { 60 | $operation = (object)$operation; 61 | } 62 | if (!is_object($operation)) { 63 | throw new Exception('Invalid patch operation - should be a JSON object'); 64 | } 65 | 66 | if (!isset($operation->op)) { 67 | throw new MissingFieldException('op', $operation); 68 | } 69 | if (!isset($operation->path)) { 70 | throw new MissingFieldException('path', $operation); 71 | } 72 | 73 | if (!is_string($operation->op)) { 74 | throw new InvalidFieldTypeException('op', 'string', $operation); 75 | } 76 | if (!is_string($operation->path)) { 77 | throw new InvalidFieldTypeException('path', 'string', $operation); 78 | } 79 | 80 | $op = null; 81 | switch ($operation->op) { 82 | case Add::OP: 83 | $op = new Add(); 84 | break; 85 | case Copy::OP: 86 | $op = new Copy(); 87 | break; 88 | case Move::OP: 89 | $op = new Move(); 90 | break; 91 | case Remove::OP: 92 | $op = new Remove(); 93 | break; 94 | case Replace::OP: 95 | $op = new Replace(); 96 | break; 97 | case Test::OP: 98 | $op = new Test(); 99 | break; 100 | default: 101 | throw new UnknownOperationException($operation); 102 | } 103 | $op->path = $operation->path; 104 | if ($op instanceof OpPathValue) { 105 | if (property_exists($operation, 'value')) { 106 | $op->value = $operation->value; 107 | } else { 108 | throw new MissingFieldException('value', $operation); 109 | } 110 | } elseif ($op instanceof OpPathFrom) { 111 | if (!isset($operation->from)) { 112 | throw new MissingFieldException('from', $operation); 113 | } elseif (!is_string($operation->from)) { 114 | throw new InvalidFieldTypeException('from', 'string', $operation); 115 | } 116 | $op->from = $operation->from; 117 | } 118 | $result->operations[] = $op; 119 | } 120 | return $result; 121 | } 122 | 123 | public static function export(JsonPatch $patch) 124 | { 125 | $result = array(); 126 | foreach ($patch->operations as $operation) { 127 | $result[] = (object)(array)$operation; 128 | } 129 | 130 | return $result; 131 | } 132 | 133 | public function op(OpPath $op) 134 | { 135 | $this->operations[] = $op; 136 | return $this; 137 | } 138 | 139 | #[\ReturnTypeWillChange] 140 | public function jsonSerialize() 141 | { 142 | return self::export($this); 143 | } 144 | 145 | /** 146 | * @param mixed $original 147 | * @param bool $stopOnError 148 | * @return Exception[] array of errors 149 | * @throws Exception 150 | */ 151 | public function apply(&$original, $stopOnError = true) 152 | { 153 | $errors = array(); 154 | foreach ($this->operations as $opIndex => $operation) { 155 | try { 156 | // track the current pointer field so we can use it for a potential PathException 157 | $pointerField = 'path'; 158 | $pathItems = JsonPointer::splitPath($operation->path); 159 | switch (true) { 160 | case $operation instanceof Add: 161 | JsonPointer::add($original, $pathItems, $operation->value, $this->flags); 162 | break; 163 | case $operation instanceof Copy: 164 | $pointerField = 'from'; 165 | $fromItems = JsonPointer::splitPath($operation->from); 166 | $value = JsonPointer::get($original, $fromItems); 167 | $pointerField = 'path'; 168 | JsonPointer::add($original, $pathItems, $value, $this->flags); 169 | break; 170 | case $operation instanceof Move: 171 | $pointerField = 'from'; 172 | $fromItems = JsonPointer::splitPath($operation->from); 173 | $value = JsonPointer::get($original, $fromItems); 174 | JsonPointer::remove($original, $fromItems, $this->flags); 175 | $pointerField = 'path'; 176 | JsonPointer::add($original, $pathItems, $value, $this->flags); 177 | break; 178 | case $operation instanceof Remove: 179 | JsonPointer::remove($original, $pathItems, $this->flags); 180 | break; 181 | case $operation instanceof Replace: 182 | JsonPointer::get($original, $pathItems); 183 | JsonPointer::remove($original, $pathItems, $this->flags); 184 | JsonPointer::add($original, $pathItems, $operation->value, $this->flags); 185 | break; 186 | case $operation instanceof Test: 187 | $value = JsonPointer::get($original, $pathItems); 188 | $diff = new JsonDiff($operation->value, $value, 189 | JsonDiff::STOP_ON_DIFF); 190 | if ($diff->getDiffCnt() !== 0) { 191 | throw new PatchTestOperationFailedException($operation, $value); 192 | } 193 | break; 194 | } 195 | } catch (JsonPointerException $jsonPointerException) { 196 | $pathException = new PathException( 197 | $jsonPointerException->getMessage(), 198 | $operation, 199 | $pointerField, 200 | $jsonPointerException->getCode() 201 | ); 202 | $pathException->setOpIndex($opIndex); 203 | if ($stopOnError) { 204 | throw $pathException; 205 | } else { 206 | $errors[] = $pathException; 207 | } 208 | } catch (Exception $exception) { 209 | if ($stopOnError) { 210 | throw $exception; 211 | } else { 212 | $errors[] = $exception; 213 | } 214 | } 215 | } 216 | return $errors; 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/JsonPatch/Add.php: -------------------------------------------------------------------------------- 1 | op = static::OP; 16 | $this->path = $path; 17 | } 18 | } -------------------------------------------------------------------------------- /src/JsonPatch/OpPathFrom.php: -------------------------------------------------------------------------------- 1 | from = $from; 13 | } 14 | } -------------------------------------------------------------------------------- /src/JsonPatch/OpPathValue.php: -------------------------------------------------------------------------------- 1 | value = $value; 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/JsonPatch/Remove.php: -------------------------------------------------------------------------------- 1 | $key; 114 | } else { 115 | if (!isset($ref->$key) && count($pathItems)) { 116 | throw new JsonPointerException('Non-existent path item: ' . $key); 117 | } else { 118 | $ref = &$ref->$key; 119 | } 120 | } 121 | } else { // null or array 122 | $intKey = filter_var($key, FILTER_VALIDATE_INT); 123 | if ($ref === null && (false === $intKey || $intKey !== 0)) { 124 | $key = (string)$key; 125 | if ($flags & self::RECURSIVE_KEY_CREATION) { 126 | $ref = new \stdClass(); 127 | $ref = &$ref->{$key}; 128 | } else { 129 | throw new JsonPointerException('Non-existent path item: ' . $key); 130 | } 131 | } elseif ([] === $ref && 0 === ($flags & self::STRICT_MODE) && false === $intKey && '-' !== $key) { 132 | $ref = new \stdClass(); 133 | $ref = &$ref->{$key}; 134 | } else { 135 | if ($flags & self::RECURSIVE_KEY_CREATION && $ref === null) $ref = array(); 136 | if ('-' === $key) { 137 | $ref = &$ref[count($ref)]; 138 | } else { 139 | if (false === $intKey) { 140 | if (0 === ($flags & self::TOLERATE_ASSOCIATIVE_ARRAYS)) { 141 | throw new JsonPointerException('Invalid key for array operation'); 142 | } 143 | $ref = &$ref[$key]; 144 | continue; 145 | } 146 | if (is_array($ref) && array_key_exists($key, $ref) && empty($pathItems)) { 147 | array_splice($ref, $intKey, 0, array($value)); 148 | } 149 | if (0 === ($flags & self::TOLERATE_ASSOCIATIVE_ARRAYS)) { 150 | if ($intKey > count($ref) && 0 === ($flags & self::RECURSIVE_KEY_CREATION)) { 151 | throw new JsonPointerException('Index is greater than number of items in array'); 152 | } elseif ($intKey < 0) { 153 | throw new JsonPointerException('Negative index'); 154 | } 155 | } 156 | 157 | $ref = &$ref[$intKey]; 158 | } 159 | } 160 | } 161 | } 162 | if ($ref !== null && $flags & self::SKIP_IF_ISSET) { 163 | return; 164 | } 165 | $ref = $value; 166 | } 167 | 168 | private static function arrayKeyExists($key, array $a) 169 | { 170 | if (array_key_exists($key, $a)) { 171 | return true; 172 | } 173 | $key = (string)$key; 174 | foreach ($a as $k => $v) { 175 | if ((string)$k === $key) { 176 | return true; 177 | } 178 | } 179 | return false; 180 | } 181 | 182 | private static function arrayGet($key, array $a) 183 | { 184 | $key = (string)$key; 185 | foreach ($a as $k => $v) { 186 | if ((string)$k === $key) { 187 | return $v; 188 | } 189 | } 190 | return false; 191 | } 192 | 193 | 194 | /** 195 | * @param mixed $holder 196 | * @param string[] $pathItems 197 | * @return bool|mixed 198 | * @throws Exception 199 | */ 200 | public static function get($holder, $pathItems) 201 | { 202 | $ref = $holder; 203 | while (null !== $key = array_shift($pathItems)) { 204 | if ($ref instanceof \stdClass) { 205 | if (PHP_VERSION_ID < 70100 && '' === $key) { 206 | throw new JsonPointerException('Empty property name is not supported by PHP <7.1', 207 | Exception::EMPTY_PROPERTY_NAME_UNSUPPORTED); 208 | } 209 | 210 | $vars = (array)$ref; 211 | if (self::arrayKeyExists($key, $vars)) { 212 | $ref = self::arrayGet($key, $vars); 213 | } else { 214 | throw new JsonPointerException('Key not found: ' . $key); 215 | } 216 | } elseif (is_array($ref)) { 217 | if (self::arrayKeyExists($key, $ref)) { 218 | $ref = $ref[$key]; 219 | } else { 220 | throw new JsonPointerException('Key not found: ' . $key); 221 | } 222 | } elseif (is_object($ref)) { 223 | if (isset($ref->$key)) { 224 | $ref = $ref->$key; 225 | } else { 226 | throw new JsonPointerException('Key not found: ' . $key); 227 | } 228 | } else { 229 | throw new JsonPointerException('Key not found: ' . $key); 230 | } 231 | } 232 | return $ref; 233 | } 234 | 235 | /** 236 | * @param mixed $holder 237 | * @param string $pointer 238 | * @return bool|mixed 239 | * @throws Exception 240 | */ 241 | public static function getByPointer($holder, $pointer) 242 | { 243 | return self::get($holder, self::splitPath($pointer)); 244 | } 245 | 246 | /** 247 | * @param mixed $holder 248 | * @param string[] $pathItems 249 | * @param int $flags 250 | * @return mixed 251 | * @throws Exception 252 | */ 253 | public static function remove(&$holder, $pathItems, $flags = 0) 254 | { 255 | $ref = &$holder; 256 | while (null !== $key = array_shift($pathItems)) { 257 | $parent = &$ref; 258 | $refKey = $key; 259 | if ($ref instanceof \stdClass) { 260 | if (property_exists($ref, $key)) { 261 | $ref = &$ref->$key; 262 | } else { 263 | throw new JsonPointerException('Key not found: ' . $key); 264 | } 265 | } elseif (is_object($ref)) { 266 | if (isset($ref->$key)) { 267 | $ref = &$ref->$key; 268 | } else { 269 | throw new JsonPointerException('Key not found: ' . $key); 270 | } 271 | } else { 272 | if (array_key_exists($key, $ref)) { 273 | $ref = &$ref[$key]; 274 | } else { 275 | throw new JsonPointerException('Key not found: ' . $key); 276 | } 277 | } 278 | } 279 | 280 | if (isset($parent) && isset($refKey)) { 281 | if ($parent instanceof \stdClass || is_object($parent)) { 282 | unset($parent->$refKey); 283 | } else { 284 | $isAssociative = false; 285 | if ($flags & self::TOLERATE_ASSOCIATIVE_ARRAYS) { 286 | $i = 0; 287 | foreach ($parent as $index => $value) { 288 | if ($i !== $index) { 289 | $isAssociative = true; 290 | break; 291 | } 292 | $i++; 293 | } 294 | } 295 | 296 | unset($parent[$refKey]); 297 | if (!$isAssociative && (int)$refKey !== count($parent)) { 298 | $parent = array_values($parent); 299 | } 300 | } 301 | } 302 | 303 | return $ref; 304 | } 305 | 306 | } 307 | -------------------------------------------------------------------------------- /src/JsonPointerException.php: -------------------------------------------------------------------------------- 1 | search = $search; 25 | $this->replace = $replace; 26 | $this->pathFilterRegex = $pathFilter; 27 | } 28 | 29 | /** 30 | * Recursively replaces all nodes equal to `search` value with `replace` value. 31 | * @param mixed $data 32 | * @return mixed 33 | * @throws Exception 34 | */ 35 | public function process($data) 36 | { 37 | $check = true; 38 | if ($this->pathFilterRegex && !preg_match($this->pathFilterRegex, $this->path)) { 39 | $check = false; 40 | } 41 | 42 | if (!is_array($data) && !is_object($data)) { 43 | if ($check && $data === $this->search) { 44 | $this->affectedPaths[] = $this->path; 45 | return $this->replace; 46 | } else { 47 | return $data; 48 | } 49 | } 50 | 51 | /** @var string[] $originalKeys */ 52 | $originalKeys = $data instanceof \stdClass ? get_object_vars($data) : $data; 53 | 54 | if ($check) { 55 | $diff = new JsonDiff($data, $this->search, JsonDiff::STOP_ON_DIFF); 56 | if ($diff->getDiffCnt() === 0) { 57 | $this->affectedPaths[] = $this->path; 58 | return $this->replace; 59 | } 60 | } 61 | 62 | $result = array(); 63 | 64 | foreach ($originalKeys as $key => $originalValue) { 65 | $path = $this->path; 66 | $pathItems = $this->pathItems; 67 | $actualKey = $key; 68 | $this->path .= '/' . JsonPointer::escapeSegment($actualKey); 69 | $this->pathItems[] = $actualKey; 70 | 71 | $result[$key] = $this->process($originalValue); 72 | 73 | $this->path = $path; 74 | $this->pathItems = $pathItems; 75 | } 76 | 77 | return $data instanceof \stdClass ? (object)$result : $result; 78 | } 79 | } -------------------------------------------------------------------------------- /src/MissingFieldException.php: -------------------------------------------------------------------------------- 1 | missingField = $missingField; 30 | $this->operation = $operation; 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function getMissingField() 37 | { 38 | return $this->missingField; 39 | } 40 | 41 | /** 42 | * @return OpPath|object 43 | */ 44 | public function getOperation() 45 | { 46 | return $this->operation; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ModifiedPathDiff.php: -------------------------------------------------------------------------------- 1 | path = $path; 12 | $this->original = $original; 13 | $this->new = $new; 14 | } 15 | 16 | /** 17 | * @var string 18 | */ 19 | public $path; 20 | 21 | /** 22 | * @var mixed 23 | */ 24 | public $original; 25 | 26 | /** 27 | * @var mixed 28 | */ 29 | public $new; 30 | } -------------------------------------------------------------------------------- /src/PatchTestOperationFailedException.php: -------------------------------------------------------------------------------- 1 | operation = $operation; 32 | $this->actualValue = $actualValue; 33 | } 34 | 35 | /** 36 | * @return Test 37 | */ 38 | public function getOperation() 39 | { 40 | return $this->operation; 41 | } 42 | 43 | /** 44 | * @return mixed 45 | */ 46 | public function getActualValue() 47 | { 48 | return $this->actualValue; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/PathException.php: -------------------------------------------------------------------------------- 1 | operation = $operation; 37 | $this->field = $field; 38 | } 39 | 40 | /** 41 | * @return OpPath 42 | */ 43 | public function getOperation() 44 | { 45 | return $this->operation; 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function getField() 52 | { 53 | return $this->field; 54 | } 55 | 56 | /** 57 | * @param int $opIndex 58 | * @return $this 59 | */ 60 | public function setOpIndex($opIndex) 61 | { 62 | $this->opIndex = $opIndex; 63 | return $this; 64 | } 65 | 66 | /** 67 | * @return int 68 | */ 69 | public function getOpIndex() 70 | { 71 | return $this->opIndex; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/UnknownOperationException.php: -------------------------------------------------------------------------------- 1 | op, $code, $previous); 27 | $this->operation = $operation; 28 | } 29 | 30 | /** 31 | * @return OpPath|object 32 | */ 33 | public function getOperation() 34 | { 35 | return $this->operation; 36 | } 37 | } 38 | --------------------------------------------------------------------------------