Passed
Push — master ( 3ea708...b1fd4c )
by Viacheslav
01:58
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
    const REARRANGE_ARRAYS = 1;
13
    const STOP_ON_DIFF = 2;
14
15
    /**
16
     * Use URI Fragment Identifier Representation will be used (example: "#/c%25d").
17
     * If not set default JSON String Representation (example: "/c%d").
18
     */
19
    const JSON_URI_FRAGMENT_ID = 4;
20
21
    private $options = 0;
22
    private $original;
23
    private $new;
24
25
    private $added;
26
    private $addedCnt = 0;
27
    private $addedPaths = array();
28
29
    private $removed;
30
    private $removedCnt = 0;
31
    private $removedPaths = array();
32
33
    private $modifiedOriginal;
34
    private $modifiedNew;
35
    private $modifiedCnt = 0;
36
    private $modifiedPaths = array();
37
38
    private $path = '';
39
    private $pathItems = array();
40
41
    private $rearranged;
42
43
    /** @var JsonPatch */
44
    private $jsonPatch;
45
46
    /**
47
     * Processor constructor.
48
     * @param mixed $original
49
     * @param mixed $new
50
     * @param int $options
51
     * @throws Exception
52
     */
53 24
    public function __construct($original, $new, $options = 0)
54
    {
55 24
        $this->jsonPatch = new JsonPatch();
56
57 24
        $this->original = $original;
58 24
        $this->new = $new;
59 24
        $this->options = $options;
60
61 24
        if ($options & self::JSON_URI_FRAGMENT_ID) {
62 1
            $this->path = '#';
63
        }
64
65 24
        $this->rearranged = $this->rearrange();
66 24
    }
67
68
    /**
69
     * Returns total number of differences
70
     * @return int
71
     */
72 18
    public function getDiffCnt()
73
    {
74 18
        return $this->addedCnt + $this->modifiedCnt + $this->removedCnt;
75
    }
76
77
    /**
78
     * Returns removals as partial value of original.
79
     * @return mixed
80
     */
81 4
    public function getRemoved()
82
    {
83 4
        return $this->removed;
84
    }
85
86
    /**
87
     * Returns list of `JSON` paths that were removed from original.
88
     * @return array
89
     */
90 4
    public function getRemovedPaths()
91
    {
92 4
        return $this->removedPaths;
93
    }
94
95
    /**
96
     * Returns number of removals.
97
     * @return int
98
     */
99 3
    public function getRemovedCnt()
100
    {
101 3
        return $this->removedCnt;
102
    }
103
104
    /**
105
     * Returns additions as partial value of new.
106
     * @return mixed
107
     */
108 1
    public function getAdded()
109
    {
110 1
        return $this->added;
111
    }
112
113
    /**
114
     * Returns number of additions.
115
     * @return int
116
     */
117
    public function getAddedCnt()
118
    {
119
        return $this->addedCnt;
120
    }
121
122
    /**
123
     * Returns list of `JSON` paths that were added to new.
124
     * @return array
125
     */
126 1
    public function getAddedPaths()
127
    {
128 1
        return $this->addedPaths;
129
    }
130
131
    /**
132
     * Returns changes as partial value of original.
133
     * @return mixed
134
     */
135 1
    public function getModifiedOriginal()
136
    {
137 1
        return $this->modifiedOriginal;
138
    }
139
140
    /**
141
     * Returns changes as partial value of new.
142
     * @return mixed
143
     */
144 1
    public function getModifiedNew()
145
    {
146 1
        return $this->modifiedNew;
147
    }
148
149
    /**
150
     * Returns number of changes.
151
     * @return int
152
     */
153 1
    public function getModifiedCnt()
154
    {
155 1
        return $this->modifiedCnt;
156
    }
157
158
    /**
159
     * Returns list of `JSON` paths that were changed from original to new.
160
     * @return array
161
     */
162 1
    public function getModifiedPaths()
163
    {
164 1
        return $this->modifiedPaths;
165
    }
166
167
    /**
168
     * Returns new value, rearranged with original order.
169
     * @return array|object
170
     */
171 3
    public function getRearranged()
172
    {
173 3
        return $this->rearranged;
174
    }
175
176
    /**
177
     * Returns JsonPatch of difference
178
     * @return JsonPatch
179
     */
180 4
    public function getPatch()
181
    {
182 4
        return $this->jsonPatch;
183
    }
184
185
    /**
186
     * @return array|null|object|\stdClass
187
     * @throws Exception
188
     */
189 24
    private function rearrange()
190
    {
191 24
        return $this->process($this->original, $this->new);
192
    }
193
194
    /**
195
     * @param mixed $original
196
     * @param mixed $new
197
     * @return array|null|object|\stdClass
198
     * @throws Exception
199
     */
200 24
    private function process($original, $new)
201
    {
202
        if (
203 24
            (!$original instanceof \stdClass && !is_array($original))
204 24
            || (!$new instanceof \stdClass && !is_array($new))
205
        ) {
206 23
            if ($original !== $new) {
207 10
                $this->modifiedCnt++;
208 10
                if ($this->options & self::STOP_ON_DIFF) {
209 4
                    return null;
210
                }
211 6
                $this->modifiedPaths [] = $this->path;
212
213 6
                $this->jsonPatch->op(new Test($this->path, $original));
214 6
                $this->jsonPatch->op(new Replace($this->path, $new));
215
216 6
                JsonPointer::add($this->modifiedOriginal, $this->pathItems, $original);
217 6
                JsonPointer::add($this->modifiedNew, $this->pathItems, $new);
218
219
            }
220 19
            return $new;
221
        }
222
223
        if (
224 16
            ($this->options & self::REARRANGE_ARRAYS)
225 16
            && is_array($original) && is_array($new)
226
        ) {
227 4
            $new = $this->rearrangeArray($original, $new);
228
        }
229
230 16
        $newArray = $new instanceof \stdClass ? get_object_vars($new) : $new;
231 16
        $newOrdered = array();
232
233 16
        $originalKeys = $original instanceof \stdClass ? get_object_vars($original) : $original;
234 16
        $isArray = is_array($original);
235 16
        $removedOffset = 0;
236
237 16
        foreach ($originalKeys as $key => $originalValue) {
238 16
            if ($this->options & self::STOP_ON_DIFF) {
239 6
                if ($this->modifiedCnt || $this->addedCnt || $this->removedCnt) {
240 1
                    return null;
241
                }
242
            }
243
244 16
            $path = $this->path;
245 16
            $pathItems = $this->pathItems;
246 16
            $actualKey = $key;
247 16
            if ($isArray) {
248 12
                $actualKey -= $removedOffset;
249
            }
250 16
            $this->path .= '/' . JsonPointer::escapeSegment($actualKey, $this->options & self::JSON_URI_FRAGMENT_ID);
0 ignored issues
show
Bug introduced by
$this->options & self::JSON_URI_FRAGMENT_ID of type integer is incompatible with the type boolean expected by parameter $isURIFragmentId of Swaggest\JsonDiff\JsonPointer::escapeSegment(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

250
            $this->path .= '/' . JsonPointer::escapeSegment($actualKey, /** @scrutinizer ignore-type */ $this->options & self::JSON_URI_FRAGMENT_ID);
Loading history...
251 16
            $this->pathItems[] = $actualKey;
252
253 16
            if (array_key_exists($key, $newArray)) {
254 15
                $newOrdered[$key] = $this->process($originalValue, $newArray[$key]);
255 15
                unset($newArray[$key]);
256
            } else {
257 11
                $this->removedCnt++;
258 11
                if ($this->options & self::STOP_ON_DIFF) {
259 2
                    return null;
260
                }
261 9
                $this->removedPaths [] = $this->path;
262 9
                if ($isArray) {
263 2
                    $removedOffset++;
264
                }
265
266 9
                $this->jsonPatch->op(new Remove($this->path));
267
268 9
                JsonPointer::add($this->removed, $this->pathItems, $originalValue);
269
            }
270 15
            $this->path = $path;
271 15
            $this->pathItems = $pathItems;
272
        }
273
274
        // additions
275 14
        foreach ($newArray as $key => $value) {
276 6
            $this->addedCnt++;
277 6
            if ($this->options & self::STOP_ON_DIFF) {
278 1
                return null;
279
            }
280 5
            $newOrdered[$key] = $value;
281 5
            $path = $this->path . '/' . JsonPointer::escapeSegment($key, $this->options & self::JSON_URI_FRAGMENT_ID);
282 5
            $pathItems = $this->pathItems;
283 5
            $pathItems[] = $key;
284 5
            JsonPointer::add($this->added, $pathItems, $value);
285 5
            $this->addedPaths [] = $path;
286
287 5
            $this->jsonPatch->op(new Add($path, $value));
288
289
        }
290
291 14
        return is_array($new) ? $newOrdered : (object)$newOrdered;
292
    }
293
294 4
    private function rearrangeArray(array $original, array $new)
295
    {
296 4
        $first = reset($original);
297 4
        if (!$first instanceof \stdClass) {
298 2
            return $new;
299
        }
300
301 4
        $uniqueKey = false;
302 4
        $uniqueIdx = array();
303
304
        // find unique key for all items
305 4
        $f = get_object_vars($first);
306 4
        foreach ($f as $key => $value) {
307 4
            if (is_array($value) || $value instanceof \stdClass) {
308
                continue;
309
            }
310
311 4
            $keyIsUnique = true;
312 4
            $uniqueIdx = array();
313 4
            foreach ($original as $item) {
314 4
                if (!$item instanceof \stdClass) {
315
                    return $new;
316
                }
317 4
                if (!isset($item->$key)) {
318
                    $keyIsUnique = false;
319
                    break;
320
                }
321 4
                $value = $item->$key;
322 4
                if ($value instanceof \stdClass || is_array($value)) {
323
                    $keyIsUnique = false;
324
                    break;
325
                }
326
327 4
                if (isset($uniqueIdx[$value])) {
328
                    $keyIsUnique = false;
329
                    break;
330
                }
331 4
                $uniqueIdx[$value] = true;
332
            }
333
334 4
            if ($keyIsUnique) {
335 4
                $uniqueKey = $key;
336 4
                break;
337
            }
338
        }
339
340 4
        if ($uniqueKey) {
341 4
            $newIdx = array();
342 4
            foreach ($new as $item) {
343 4
                if (!$item instanceof \stdClass) {
344
                    return $new;
345
                }
346
347 4
                if (!property_exists($item, $uniqueKey)) {
348
                    return $new;
349
                }
350
351 4
                $value = $item->$uniqueKey;
352
353 4
                if ($value instanceof \stdClass || is_array($value)) {
354
                    return $new;
355
                }
356
357 4
                if (isset($newIdx[$value])) {
358
                    return $new;
359
                }
360
361 4
                $newIdx[$value] = $item;
362
            }
363
364 4
            $newRearranged = array();
365 4
            foreach ($uniqueIdx as $key => $item) {
366 4
                if (isset($newIdx[$key])) {
367 4
                    $newRearranged [] = $newIdx[$key];
368 4
                    unset($newIdx[$key]);
369
                }
370
            }
371 4
            foreach ($newIdx as $item) {
372
                $newRearranged [] = $item;
373
            }
374 4
            return $newRearranged;
375
        }
376
377
        return $new;
378
    }
379
}