JsonDiff::rearrangeArray()   F
last analyzed

Complexity

Conditions 23
Paths 394

Size

Total Lines 111
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 39.8671

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 23
eloc 63
c 3
b 0
f 0
nc 394
nop 2
dl 0
loc 111
ccs 28
cts 41
cp 0.6828
crap 39.8671
rs 0.9083

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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