Completed
Pull Request — master (#19)
by
unknown
02:53
created

JsonDiff::getModifiedDiff()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Swaggest\JsonDiff;
4
5
use Swaggest\JsonDiff\JsonPatch\Add;
6
use Swaggest\JsonDiff\JsonPatch\Remove;
7
use Swaggest\JsonDiff\JsonPatch\Replace;
8
use Swaggest\JsonDiff\JsonPatch\Test;
9
10
class JsonDiff
11
{
12
    /**
13
     * REARRANGE_ARRAYS is an option to enable arrays rearrangement to minimize the difference.
14
     */
15
    const REARRANGE_ARRAYS = 1;
16
17
    /**
18
     * STOP_ON_DIFF is an option to improve performance by stopping comparison when a difference is found.
19
     */
20
    const STOP_ON_DIFF = 2;
21
22
    /**
23
     * JSON_URI_FRAGMENT_ID is an option to use URI Fragment Identifier Representation (example: "#/c%25d").
24
     * If not set default JSON String Representation (example: "/c%d").
25
     */
26
    const JSON_URI_FRAGMENT_ID = 4;
27
28
    /**
29
     * SKIP_JSON_PATCH is an option to improve performance by not building JsonPatch for this diff.
30
     */
31
    const SKIP_JSON_PATCH = 8;
32
33
    /**
34
     * SKIP_JSON_MERGE_PATCH is an option to improve performance by not building JSON Merge Patch value for this diff.
35
     */
36
    const SKIP_JSON_MERGE_PATCH = 16;
37
38
    private $options = 0;
39
    private $original;
40
    private $new;
41
42
    /**
43
     * @var mixed Merge patch container
44
     */
45
    private $merge;
46
47
    private $added;
48
    private $addedCnt = 0;
49
    private $addedPaths = array();
50
51
    private $removed;
52
    private $removedCnt = 0;
53
    private $removedPaths = array();
54
55
    private $modifiedOriginal;
56
    private $modifiedNew;
57
    private $modifiedCnt = 0;
58
    private $modifiedPaths = array();
59
    private $modifiedDiff = array();
60
61
    private $path = '';
62
    private $pathItems = array();
63
64
    private $rearranged;
65
66
    /** @var JsonPatch */
67
    private $jsonPatch;
68
69
    /**
70
     * @param mixed $original
71
     * @param mixed $new
72
     * @param int $options
73
     * @throws Exception
74
     */
75 47
    public function __construct($original, $new, $options = 0)
76
    {
77 47
        if (!($options & self::SKIP_JSON_PATCH)) {
78 47
            $this->jsonPatch = new JsonPatch();
79
        }
80
81 47
        $this->original = $original;
82 47
        $this->new = $new;
83 47
        $this->options = $options;
84
85 47
        if ($options & self::JSON_URI_FRAGMENT_ID) {
86 1
            $this->path = '#';
87
        }
88
89 47
        $this->rearranged = $this->rearrange();
90 47
        if (($new !== null) && $this->merge === null) {
91 19
            $this->merge = new \stdClass();
92
        }
93 47
    }
94
95
    /**
96
     * Returns total number of differences
97
     * @return int
98
     */
99 20
    public function getDiffCnt()
100
    {
101 20
        return $this->addedCnt + $this->modifiedCnt + $this->removedCnt;
102
    }
103
104
    /**
105
     * Returns removals as partial value of original.
106
     * @return mixed
107
     */
108 4
    public function getRemoved()
109
    {
110 4
        return $this->removed;
111
    }
112
113
    /**
114
     * Returns list of `JSON` paths that were removed from original.
115
     * @return array
116
     */
117 4
    public function getRemovedPaths()
118
    {
119 4
        return $this->removedPaths;
120
    }
121
122
    /**
123
     * Returns number of removals.
124
     * @return int
125
     */
126 4
    public function getRemovedCnt()
127
    {
128 4
        return $this->removedCnt;
129
    }
130
131
    /**
132
     * Returns additions as partial value of new.
133
     * @return mixed
134
     */
135 1
    public function getAdded()
136
    {
137 1
        return $this->added;
138
    }
139
140
    /**
141
     * Returns number of additions.
142
     * @return int
143
     */
144 1
    public function getAddedCnt()
145
    {
146 1
        return $this->addedCnt;
147
    }
148
149
    /**
150
     * Returns list of `JSON` paths that were added to new.
151
     * @return array
152
     */
153 1
    public function getAddedPaths()
154
    {
155 1
        return $this->addedPaths;
156
    }
157
158
    /**
159
     * Returns changes as partial value of original.
160
     * @return mixed
161
     */
162 1
    public function getModifiedOriginal()
163
    {
164 1
        return $this->modifiedOriginal;
165
    }
166
167
    /**
168
     * Returns changes as partial value of new.
169
     * @return mixed
170
     */
171 1
    public function getModifiedNew()
172
    {
173 1
        return $this->modifiedNew;
174
    }
175
176
    /**
177
     * Returns number of changes.
178
     * @return int
179
     */
180 2
    public function getModifiedCnt()
181
    {
182 2
        return $this->modifiedCnt;
183
    }
184
185
    /**
186
     * Returns list of `JSON` paths that were changed from original to new.
187
     * @return array
188
     */
189 1
    public function getModifiedPaths()
190
    {
191 1
        return $this->modifiedPaths;
192
    }
193
194
    /**
195
     * Returns list of paths with original and new values.
196
     * @return array
197
     */
198 1
    public function getModifiedDiff()
199
    {
200 1
        return $this->modifiedDiff;
201
    }
202
203
    /**
204
     * Returns new value, rearranged with original order.
205
     * @return array|object
206
     */
207 3
    public function getRearranged()
208
    {
209 3
        return $this->rearranged;
210
    }
211
212
    /**
213
     * Returns JsonPatch of difference
214
     * @return JsonPatch
215
     */
216 6
    public function getPatch()
217
    {
218 6
        return $this->jsonPatch;
219
    }
220
221
    /**
222
     * Returns JSON Merge Patch value of difference
223
     */
224 19
    public function getMergePatch()
225
    {
226 19
        return $this->merge;
227
228
    }
229
230
    /**
231
     * @return array|null|object|\stdClass
232
     * @throws Exception
233
     */
234 47
    private function rearrange()
235
    {
236 47
        return $this->process($this->original, $this->new);
237
    }
238
239
    /**
240
     * @param mixed $original
241
     * @param mixed $new
242
     * @return array|null|object|\stdClass
243
     * @throws Exception
244
     */
245 47
    private function process($original, $new)
246
    {
247 47
        $merge = !($this->options & self::SKIP_JSON_MERGE_PATCH);
248
249
        if (
250 47
            (!$original instanceof \stdClass && !is_array($original))
251 47
            || (!$new instanceof \stdClass && !is_array($new))
252
        ) {
253 39
            if ($original !== $new) {
254 20
                $this->modifiedCnt++;
255 20
                if ($this->options & self::STOP_ON_DIFF) {
256 4
                    return null;
257
                }
258 16
                $this->modifiedPaths [] = $this->path;
259
260 16
                if ($this->jsonPatch !== null) {
261 16
                    $this->jsonPatch->op(new Test($this->path, $original));
262 16
                    $this->jsonPatch->op(new Replace($this->path, $new));
263
                }
264
265 16
                JsonPointer::add($this->modifiedOriginal, $this->pathItems, $original);
266 16
                JsonPointer::add($this->modifiedNew, $this->pathItems, $new);
267
268 16
                if ($merge) {
269 16
                    JsonPointer::add($this->merge, $this->pathItems, $new, JsonPointer::RECURSIVE_KEY_CREATION);
270
                }
271
272 16
                $this->modifiedDiff[] = [
273 16
                    'path' => $this->path,
274 16
                    'original' => $original,
275 16
                    'new' => $new,
276
                ];
277
            }
278 35
            return $new;
279
        }
280
281
        if (
282 37
            ($this->options & self::REARRANGE_ARRAYS)
283 37
            && is_array($original) && is_array($new)
284
        ) {
285 5
            $new = $this->rearrangeArray($original, $new);
286
        }
287
288 37
        $newArray = $new instanceof \stdClass ? get_object_vars($new) : $new;
289 37
        $newOrdered = array();
290
291 37
        $originalKeys = $original instanceof \stdClass ? get_object_vars($original) : $original;
292 37
        $isArray = is_array($original);
293 37
        $removedOffset = 0;
294
295 37
        if ($merge && is_array($new) && !is_array($original)) {
296 2
            $merge = false;
297 2
            JsonPointer::add($this->merge, $this->pathItems, $new);
298 35
        } elseif ($merge  && $new instanceof \stdClass && !$original instanceof \stdClass) {
299 4
            $merge = false;
300 4
            JsonPointer::add($this->merge, $this->pathItems, $new);
301
        }
302
303 37
        $isUriFragment = (bool)($this->options & self::JSON_URI_FRAGMENT_ID);
304 37
        foreach ($originalKeys as $key => $originalValue) {
305 36
            if ($this->options & self::STOP_ON_DIFF) {
306 7
                if ($this->modifiedCnt || $this->addedCnt || $this->removedCnt) {
307 1
                    return null;
308
                }
309
            }
310
311 36
            $path = $this->path;
312 36
            $pathItems = $this->pathItems;
313 36
            $actualKey = $key;
314 36
            if ($isArray && is_int($actualKey)) {
315 16
                $actualKey -= $removedOffset;
316
            }
317 36
            $this->path .= '/' . JsonPointer::escapeSegment((string)$actualKey, $isUriFragment);
318 36
            $this->pathItems[] = $actualKey;
319
320 36
            if (array_key_exists($key, $newArray)) {
321 30
                $newOrdered[$key] = $this->process($originalValue, $newArray[$key]);
322 30
                unset($newArray[$key]);
323
            } else {
324 18
                $this->removedCnt++;
325 18
                if ($this->options & self::STOP_ON_DIFF) {
326 2
                    return null;
327
                }
328 16
                $this->removedPaths [] = $this->path;
329 16
                if ($isArray) {
330 4
                    $removedOffset++;
331
                }
332
333 16
                if ($this->jsonPatch !== null) {
334 16
                    $this->jsonPatch->op(new Remove($this->path));
335
                }
336
337 16
                JsonPointer::add($this->removed, $this->pathItems, $originalValue);
338 16
                if ($merge) {
339 13
                    JsonPointer::add($this->merge, $this->pathItems, null);
340
                }
341
342
            }
343 35
            $this->path = $path;
344 35
            $this->pathItems = $pathItems;
345
        }
346
347
        // additions
348 35
        foreach ($newArray as $key => $value) {
349 15
            $this->addedCnt++;
350 15
            if ($this->options & self::STOP_ON_DIFF) {
351 2
                return null;
352
            }
353 13
            $newOrdered[$key] = $value;
354 13
            $path = $this->path . '/' . JsonPointer::escapeSegment($key, $isUriFragment);
355 13
            $pathItems = $this->pathItems;
356 13
            $pathItems[] = $key;
357 13
            JsonPointer::add($this->added, $pathItems, $value);
358 13
            if ($merge) {
359 9
                JsonPointer::add($this->merge, $pathItems, $value);
360
            }
361
362 13
            $this->addedPaths [] = $path;
363
364 13
            if ($this->jsonPatch !== null) {
365 13
                $this->jsonPatch->op(new Add($path, $value));
366
            }
367
368
        }
369
370 35
        return is_array($new) ? $newOrdered : (object)$newOrdered;
371
    }
372
373 5
    private function rearrangeArray(array $original, array $new)
374
    {
375 5
        $first = reset($original);
376 5
        if (!$first instanceof \stdClass) {
377 3
            return $new;
378
        }
379
380 4
        $uniqueKey = false;
381 4
        $uniqueIdx = array();
382
383
        // find unique key for all items
384
        /** @var mixed[string]  $f */
385 4
        $f = get_object_vars($first);
386 4
        foreach ($f as $key => $value) {
387 4
            if (is_array($value) || $value instanceof \stdClass) {
388
                continue;
389
            }
390
391 4
            $keyIsUnique = true;
392 4
            $uniqueIdx = array();
393 4
            foreach ($original as $item) {
394 4
                if (!$item instanceof \stdClass) {
395
                    return $new;
396
                }
397 4
                if (!isset($item->$key)) {
398
                    $keyIsUnique = false;
399
                    break;
400
                }
401 4
                $value = $item->$key;
402 4
                if ($value instanceof \stdClass || is_array($value)) {
403
                    $keyIsUnique = false;
404
                    break;
405
                }
406
407 4
                if (isset($uniqueIdx[$value])) {
408
                    $keyIsUnique = false;
409
                    break;
410
                }
411 4
                $uniqueIdx[$value] = true;
412
            }
413
414 4
            if ($keyIsUnique) {
415 4
                $uniqueKey = $key;
416 4
                break;
417
            }
418
        }
419
420 4
        if ($uniqueKey) {
0 ignored issues
show
introduced by
$uniqueKey is of type mixed, thus it always evaluated to false.
Loading history...
421 4
            $newIdx = array();
422 4
            foreach ($new as $item) {
423 4
                if (!$item instanceof \stdClass) {
424
                    return $new;
425
                }
426
427 4
                if (!property_exists($item, $uniqueKey)) {
428
                    return $new;
429
                }
430
431 4
                $value = $item->$uniqueKey;
432
433 4
                if ($value instanceof \stdClass || is_array($value)) {
434
                    return $new;
435
                }
436
437 4
                if (isset($newIdx[$value])) {
438
                    return $new;
439
                }
440
441 4
                $newIdx[$value] = $item;
442
            }
443
444 4
            $newRearranged = array();
445 4
            foreach ($uniqueIdx as $key => $item) {
446 4
                if (isset($newIdx[$key])) {
447 4
                    $newRearranged [] = $newIdx[$key];
448 4
                    unset($newIdx[$key]);
449
                }
450
            }
451 4
            foreach ($newIdx as $item) {
452
                $newRearranged [] = $item;
453
            }
454 4
            return $newRearranged;
455
        }
456
457
        return $new;
458
    }
459
}