Completed
Push — master ( 1282c4...de18f5 )
by Viacheslav
17:22 queued 03:56
created

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