JsonDiff   F
last analyzed

Complexity

Total Complexity 101

Size/Duplication

Total Lines 558
Duplicated Lines 0 %

Test Coverage

Coverage 92.05%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
eloc 228
c 6
b 0
f 0
dl 0
loc 558
ccs 162
cts 176
cp 0.9205
rs 2
wmc 101

19 Methods

Rating   Name   Duplication   Size   Complexity  
A getDiffCnt() 0 3 1
A getMergePatch() 0 3 1
A getModifiedOriginal() 0 3 1
A getRemoved() 0 3 1
A getRemovedPaths() 0 3 1
F rearrangeArray() 0 111 23
A getRearranged() 0 3 1
A getRemovedCnt() 0 3 1
B rearrangeEqualItems() 0 42 8
F process() 0 143 50
A getPatch() 0 3 1
A getAdded() 0 3 1
A getModifiedNew() 0 3 1
A getAddedCnt() 0 3 1
A __construct() 0 17 5
A getModifiedCnt() 0 3 1
A getModifiedDiff() 0 3 1
A getModifiedPaths() 0 3 1
A getAddedPaths() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like JsonDiff often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use JsonDiff, and based on these observations, apply Extract Interface, too.

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