Skip to content

Commit 5fb0a83

Browse files
committedFeb 13, 2018
Improve api with readme driven development
1 parent 2478c8d commit 5fb0a83

File tree

6 files changed

+342
-58
lines changed

6 files changed

+342
-58
lines changed
 

‎README.md

+74
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,77 @@
11
# JSON Diff
22

33
Get a readable diff from two bits of JSON.
4+
5+
## Installation
6+
7+
`composer require konsulting/json-diff`
8+
9+
## Usage
10+
11+
There are a few ways to use the class. It depends on your preference, see the example below.
12+
It is also possible to exclude a set of keys from the diff, also shown below.
13+
14+
```php
15+
<?php
16+
17+
use Konsulting\JsonDiff;
18+
19+
// Using 'new'
20+
$diff = (new JsonDiff('["json"]'))->exclude(['key'])->compareTo('["different_json"]');
21+
22+
// Using a simple factory method
23+
$diff = JsonDiff::original('["json"]')->exclude(['key'])->compareTo('["different_json"]');
24+
25+
// Using a simple 'all-in-one' static method.
26+
$diff = JsonDiff::compare($original = '["json"]', $new = '["different_json"]', $exclude = ['key']);
27+
28+
```
29+
30+
The output is a JsonDiffResult that stores the following information:
31+
32+
- *original* - the original json as an array
33+
- *new* - the new json as an array
34+
- *added* - what was added
35+
- *removed* - what was removed
36+
- *diff* - a combined diff of added and removed as an array
37+
- *changed* - a boolean signifying if there was a relevant change
38+
39+
The JsonDiffResult allows access of the result in several ways:
40+
41+
```php
42+
<?php
43+
$diff->toArray();
44+
45+
[
46+
'original' => ['...'],
47+
'new' => ['...'],
48+
'diff' => [
49+
'added' => ['...'],
50+
'removed' => ['...']
51+
],
52+
'changed' => true // or false if nothing changed
53+
];
54+
55+
$diff->toJson(); // Json representation of toArray()
56+
57+
// All the properties can be accessed using array or object notation;
58+
// original, new, added, removed, diff, changed.
59+
60+
$diff->diff;
61+
$diff['diff'];
62+
```
63+
64+
## Contributing
65+
66+
Contributions are welcome and will be fully credited. We will accept contributions by Pull Request.
67+
68+
Please:
69+
70+
* Use the PSR-2 Coding Standard
71+
* Add tests, if you’re not sure how, please ask.
72+
* Document changes in behaviour, including readme.md.
73+
74+
## Testing
75+
We use [PHPUnit](https://phpunit.de)
76+
77+
Run tests using PHPUnit: `vendor/bin/phpunit`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
namespace Konsulting\Exceptions;
3+
4+
use Exception;
5+
6+
class CannotChangeDiffResult extends Exception
7+
{
8+
9+
}

‎src/JsonDiff.php

+100-52
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class JsonDiff
99
protected $exclude = [];
1010
protected $original;
1111
protected $new;
12+
protected $divider = '||';
1213

1314
/**
1415
* JsonDiff constructor.
@@ -21,6 +22,8 @@ public function __construct($original = '')
2122
}
2223

2324
/**
25+
* Set indices to ignore when checking the diff
26+
*
2427
* @param string|array $value
2528
*
2629
* @return $this
@@ -35,30 +38,62 @@ public function exclude($value)
3538
}
3639

3740
/**
41+
* Set the divider used for the flatten and inflate operations
42+
*
43+
* @param null $divider
44+
*
45+
* @return $this
46+
*/
47+
public function setDivider($divider = null)
48+
{
49+
$this->divider = $divider ?: '||';
50+
51+
return $this;
52+
}
53+
54+
/**
55+
* Simple factory method for cleaner usage
56+
*
57+
* @param $original
58+
*
59+
* @return static
60+
*/
61+
public static function original($original)
62+
{
63+
return new static($original);
64+
}
65+
66+
/**
67+
* Simple call to do everything tidyly
68+
*
3869
* @param string $original
3970
* @param string $new
71+
* @param array $exclude
4072
*
4173
* @return array
42-
* @throws \Exception
74+
* @throws \Konsulting\Exceptions\JsonDecodeFailed
4375
*/
44-
public function compare($original, $new)
76+
public static function compare($original, $new, $exclude = [])
4577
{
46-
$this->original = $original;
47-
48-
return $this->compareTo($new);
78+
return static::original($original)->exclude($exclude)->compareTo($new);
4979
}
5080

5181
/**
82+
* Perform comparison of the original to this new JSON.
83+
*
5284
* @param string $new
5385
*
54-
* @return array
86+
* @return JsonDiffResult
5587
* @throws \Konsulting\Exceptions\JsonDecodeFailed
5688
*/
5789
public function compareTo($new)
5890
{
5991
$original = $this->decode($this->original);
6092
$new = $this->decode($new);
6193

94+
// we flatten the whole array into a single level array with the
95+
// indices representing the full depth, this lets us diff in
96+
// a good level of detail.
6297
$flatOriginal = $this->flatten($original);
6398
$flatNew = $this->flatten($new);
6499

@@ -68,17 +103,12 @@ public function compareTo($new)
68103
$added = array_diff_assoc($flatNew, $flatOriginal);
69104
$removed = array_diff_assoc($flatOriginal, $flatNew);
70105

71-
$diff = [
72-
'added' => $this->inflate($added),
73-
'removed' => $this->inflate($removed),
74-
];
75-
76-
$changed = ! empty($added) || ! empty($removed);
77-
78-
return compact('original', 'new', 'diff', 'changed');
106+
return new JsonDiffResult($original, $new, $this->inflate($added), $this->inflate($removed));
79107
}
80108

81109
/**
110+
* Try to decode the JSON we were passed
111+
*
82112
* @param $json
83113
*
84114
* @return array
@@ -96,6 +126,8 @@ public function decode($json)
96126
}
97127

98128
/**
129+
* Strip data that we want to ignore for diff-ing. It is a deep operation, digging into the data.
130+
*
99131
* @param array $columns
100132
* @param array $array
101133
*
@@ -119,60 +151,76 @@ public function stripColumns($columns = [], $array = [])
119151
}
120152

121153
/**
122-
* @param $arr
154+
* Flatten the json into a single level array with keys that represent the nested data positions.
155+
*
156+
* @param $toFlatten
123157
* @param string $base
124-
* @param string $divider_char
125158
*
126159
* @return array
127160
*/
128-
public function flatten($arr, $base = "", $divider_char = "||")
161+
public function flatten($toFlatten, $base = "")
129162
{
130-
$ret = [];
131-
if (is_array($arr)) {
132-
foreach ($arr as $k => $v) {
133-
if (is_array($v)) {
134-
$tmp_array = $this->flatten($v, $base.$k.$divider_char, $divider_char);
135-
$ret = array_merge($ret, $tmp_array);
136-
} else {
137-
$ret[$base.$k] = $v;
138-
}
139-
}
163+
if (! is_array($toFlatten)) {
164+
return $toFlatten;
165+
}
166+
167+
$flattened = [];
168+
foreach ($toFlatten as $key => $value) {
169+
$flattenedValue = $this->flatten($value, $base.$key.$this->divider);
170+
171+
$flattened = array_merge(
172+
$flattened,
173+
is_array($flattenedValue) ? $flattenedValue : [$base.$key => $flattenedValue]
174+
);
140175
}
141-
return $ret;
176+
177+
return $flattened;
142178
}
143179

144180
/**
145-
* @param $arr
146-
* @param string $divider_char
181+
* Inflate the single level array back up to a multi-dimensional PHP array
182+
*
183+
* @param $toInflate
147184
*
148-
* @return array|bool
185+
* @return mixed
149186
*/
150-
public function inflate($arr, $divider_char = "||")
187+
public function inflate($toInflate)
151188
{
152-
if (!is_array($arr)) {
153-
return false;
189+
if (!is_array($toInflate)) {
190+
return $toInflate;
154191
}
155192

156-
$split = '/' . preg_quote($divider_char, '/') . '/';
157-
158-
$ret = [];
159-
foreach ($arr as $key => $val) {
160-
$parts = preg_split($split, $key, -1, PREG_SPLIT_NO_EMPTY);
161-
$leafpart = array_pop($parts);
162-
$parent = &$ret;
163-
foreach ($parts as $part) {
164-
if (!isset($parent[$part])) {
165-
$parent[$part] = [];
166-
} elseif (!is_array($parent[$part])) {
167-
$parent[$part] = [];
168-
}
169-
$parent = &$parent[$part];
170-
}
193+
$inflated = [];
194+
195+
foreach ($toInflate as $complexKey => $value) {
196+
$this->inflateByComplexKey($inflated, $complexKey, $value);
197+
}
171198

172-
if (empty($parent[$leafpart])) {
173-
$parent[$leafpart] = $val;
199+
return $inflated;
200+
}
201+
202+
/**
203+
* Add a value to the array, by working through the nesting and popping the
204+
* value in the correct place. The array is passed by reference and
205+
* worked on directly.
206+
*
207+
* @param $inflated
208+
* @param $complexKey
209+
* @param $value
210+
*/
211+
protected function inflateByComplexKey(&$inflated, $complexKey, $value)
212+
{
213+
$divider = '/' . preg_quote($this->divider, '/') . '/';
214+
$keys = preg_split($divider, $complexKey, -1, PREG_SPLIT_NO_EMPTY);
215+
$finalKey = array_pop($keys);
216+
217+
foreach ($keys as $key) {
218+
if (!isset($inflated[$key])) {
219+
$inflated[$key] = [];
174220
}
221+
$inflated = &$inflated[$key];
175222
}
176-
return $ret;
223+
224+
$inflated[$finalKey] = $value;
177225
}
178226
}

‎src/JsonDiffResult.php

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace Konsulting;
4+
5+
use ArrayAccess;
6+
use Konsulting\Exceptions\CannotChangeDiffResult;
7+
8+
class JsonDiffResult implements ArrayAccess
9+
{
10+
protected $original;
11+
protected $new;
12+
protected $added = [];
13+
protected $removed = [];
14+
protected $changed = false;
15+
protected $diff = [];
16+
17+
public function __construct($original, $new, $added = [], $removed = [])
18+
{
19+
$this->original = $original;
20+
$this->new = $new;
21+
$this->added = $added;
22+
$this->removed = $removed;
23+
$this->changed = !empty($this->added) || !empty($this->removed);
24+
25+
$this->diff = $this->diff();
26+
}
27+
28+
protected function diff()
29+
{
30+
return [
31+
'added' => $this->added,
32+
'removed' => $this->removed,
33+
];
34+
}
35+
36+
public function toArray()
37+
{
38+
return [
39+
'original' => $this->original,
40+
'new' => $this->new,
41+
'diff' => $this->diff,
42+
'changed' => $this->changed,
43+
];
44+
}
45+
46+
public function toJson($options = JSON_PRETTY_PRINT | JSON_NUMERIC_CHECK)
47+
{
48+
return json_encode($this->toArray(), $options);
49+
}
50+
51+
public function __toString()
52+
{
53+
return $this->toJson();
54+
}
55+
56+
public function __get($name)
57+
{
58+
return isset($this->{$name}) ? $this->{$name} : null;
59+
}
60+
61+
// Array Access
62+
public function offsetExists($offset)
63+
{
64+
return isset($this->{$offset}) ? $this->{$offset} : null;
65+
}
66+
67+
public function offsetGet($offset)
68+
{
69+
return isset($this->{$offset}) ? $this->{$offset} : null;
70+
}
71+
72+
public function offsetSet($offset, $value)
73+
{
74+
throw new CannotChangeDiffResult;
75+
}
76+
77+
public function offsetUnset($offset) {
78+
throw new CannotChangeDiffResult;
79+
}
80+
}

0 commit comments

Comments
 (0)
Please sign in to comment.