Passed
Pull Request — master (#15)
by
unknown
03:21
created

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