Skip to content

Commit de18f54

Browse files
authored
Merge pull request #21 from swaggest/assoc-array
Add support for PHP associative arrays as JSON objects, resolves #17
2 parents 1282c40 + 58eaea4 commit de18f54

File tree

6 files changed

+123
-11
lines changed

6 files changed

+123
-11
lines changed

Diff for: .travis.yml

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ language: php
22
php:
33
- nightly
44
- hhvm
5+
- 7.3
56
- 7.2
67
- 7.1
78
- 7.0

Diff for: README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Available options:
6262
* `JSON_URI_FRAGMENT_ID` is an option to use URI Fragment Identifier Representation (example: "#/c%25d"). If not set default JSON String Representation (example: "/c%d").
6363
* `SKIP_JSON_PATCH` is an option to improve performance by not building JsonPatch for this diff.
6464
* `SKIP_JSON_MERGE_PATCH` is an option to improve performance by not building JSON Merge Patch value for this diff.
65+
* `TOLERATE_ASSOCIATIVE_ARRAYS` is an option to allow associative arrays to mimic JSON objects (not recommended).
6566

6667
Options can be combined, e.g. `JsonDiff::REARRANGE_ARRAYS + JsonDiff::STOP_ON_DIFF`.
6768

@@ -123,9 +124,10 @@ Applies patch to `JSON`-decoded data.
123124
#### `setFlags`
124125
Alters default behavior.
125126

126-
Available flag:
127+
Available flags:
127128

128129
* `JsonPatch::STRICT_MODE` Disallow converting empty array to object for key creation.
130+
* `JsonPatch::TOLERATE_ASSOCIATIVE_ARRAYS` Allow associative arrays to mimic JSON objects (not recommended).
129131

130132
### `JsonPointer`
131133

Diff for: src/JsonDiff.php

+15
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ class JsonDiff
3535
*/
3636
const SKIP_JSON_MERGE_PATCH = 16;
3737

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+
3843
private $options = 0;
3944
private $original;
4045
private $new;
@@ -236,6 +241,16 @@ private function process($original, $new)
236241
{
237242
$merge = !($this->options & self::SKIP_JSON_MERGE_PATCH);
238243

244+
if ($this->options & self::TOLERATE_ASSOCIATIVE_ARRAYS) {
245+
if (is_array($original) && !empty($original) && !array_key_exists(0, $original)) {
246+
$original = (object)$original;
247+
}
248+
249+
if (is_array($new) && !empty($new) && !array_key_exists(0, $new)) {
250+
$new = (object)$new;
251+
}
252+
}
253+
239254
if (
240255
(!$original instanceof \stdClass && !is_array($original))
241256
|| (!$new instanceof \stdClass && !is_array($new))

Diff for: src/JsonPatch.php

+9-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ class JsonPatch implements \JsonSerializable
2525
*/
2626
const STRICT_MODE = 2;
2727

28+
/**
29+
* Allow associative arrays to mimic JSON objects (not recommended)
30+
*/
31+
const TOLERATE_ASSOCIATIVE_ARRAYS = 8;
32+
33+
2834
private $flags = 0;
2935

3036
/**
@@ -146,15 +152,15 @@ public function apply(&$original, $stopOnError = true)
146152
case $operation instanceof Move:
147153
$fromItems = JsonPointer::splitPath($operation->from);
148154
$value = JsonPointer::get($original, $fromItems);
149-
JsonPointer::remove($original, $fromItems);
155+
JsonPointer::remove($original, $fromItems, $this->flags);
150156
JsonPointer::add($original, $pathItems, $value, $this->flags);
151157
break;
152158
case $operation instanceof Remove:
153-
JsonPointer::remove($original, $pathItems);
159+
JsonPointer::remove($original, $pathItems, $this->flags);
154160
break;
155161
case $operation instanceof Replace:
156162
JsonPointer::get($original, $pathItems);
157-
JsonPointer::remove($original, $pathItems);
163+
JsonPointer::remove($original, $pathItems, $this->flags);
158164
JsonPointer::add($original, $pathItems, $operation->value, $this->flags);
159165
break;
160166
case $operation instanceof Test:

Diff for: src/JsonPointer.php

+33-7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ class JsonPointer
2020
*/
2121
const SKIP_IF_ISSET = 4;
2222

23+
/**
24+
* Allow associative arrays to mimic JSON objects (not recommended)
25+
*/
26+
const TOLERATE_ASSOCIATIVE_ARRAYS = 8;
27+
2328
/**
2429
* @param string $key
2530
* @param bool $isURIFragmentId
@@ -135,12 +140,18 @@ public static function add(&$holder, $pathItems, $value, $flags = self::RECURSIV
135140
array_splice($ref, $key, 0, array($value));
136141
}
137142
if (false === $intKey) {
138-
throw new Exception('Invalid key for array operation');
143+
if (0 === ($flags & self::TOLERATE_ASSOCIATIVE_ARRAYS)) {
144+
throw new Exception('Invalid key for array operation');
145+
}
146+
$ref = &$ref[$key];
147+
continue;
139148
}
140-
if ($intKey > count($ref) && 0 === ($flags & self::RECURSIVE_KEY_CREATION)) {
141-
throw new Exception('Index is greater than number of items in array');
142-
} elseif ($intKey < 0) {
143-
throw new Exception('Negative index');
149+
if (0 === ($flags & self::TOLERATE_ASSOCIATIVE_ARRAYS)) {
150+
if ($intKey > count($ref) && 0 === ($flags & self::RECURSIVE_KEY_CREATION)) {
151+
throw new Exception('Index is greater than number of items in array');
152+
} elseif ($intKey < 0) {
153+
throw new Exception('Negative index');
154+
}
144155
}
145156

146157
$ref = &$ref[$intKey];
@@ -235,10 +246,11 @@ public static function getByPointer($holder, $pointer)
235246
/**
236247
* @param mixed $holder
237248
* @param string[] $pathItems
249+
* @param int $flags
238250
* @return mixed
239251
* @throws Exception
240252
*/
241-
public static function remove(&$holder, $pathItems)
253+
public static function remove(&$holder, $pathItems, $flags = 0)
242254
{
243255
$ref = &$holder;
244256
while (null !== $key = array_shift($pathItems)) {
@@ -269,12 +281,26 @@ public static function remove(&$holder, $pathItems)
269281
if ($parent instanceof \stdClass || is_object($parent)) {
270282
unset($parent->$refKey);
271283
} else {
284+
$isAssociative = false;
285+
$ff = $flags & self::TOLERATE_ASSOCIATIVE_ARRAYS;
286+
if ($flags & self::TOLERATE_ASSOCIATIVE_ARRAYS) {
287+
$i = 0;
288+
foreach ($parent as $index => $value) {
289+
if ($i !== $index) {
290+
$isAssociative = true;
291+
break;
292+
}
293+
}
294+
}
295+
272296
unset($parent[$refKey]);
273-
if ($refKey !== count($parent)) {
297+
if (!$isAssociative && (int)$refKey !== count($parent)) {
274298
$parent = array_values($parent);
275299
}
276300
}
277301
}
302+
278303
return $ref;
279304
}
305+
280306
}

Diff for: tests/src/AssociativeTest.php

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace Swaggest\JsonDiff\Tests;
4+
5+
6+
use Swaggest\JsonDiff\JsonDiff;
7+
use Swaggest\JsonDiff\JsonPatch;
8+
9+
class AssociativeTest extends \PHPUnit_Framework_TestCase
10+
{
11+
/**
12+
* @throws \Swaggest\JsonDiff\Exception
13+
*/
14+
public function testDiffAssociative()
15+
{
16+
$originalJson = <<<'JSON'
17+
{
18+
"key1": [4, 1, 2, 3],
19+
"key2": 2,
20+
"key3": {
21+
"sub0": 0,
22+
"sub1": "a",
23+
"sub2": "b"
24+
},
25+
"key4": [
26+
{"a":1, "b":true, "subs": [{"s":1}, {"s":2}, {"s":3}]}, {"a":2, "b":false}, {"a":3}
27+
]
28+
}
29+
JSON;
30+
31+
$newJson = <<<'JSON'
32+
{
33+
"key5": "wat",
34+
"key1": [5, 1, 2, 3],
35+
"key4": [
36+
{"c":false, "a":2}, {"a":1, "b":true, "subs": [{"s":3, "add": true}, {"s":2}, {"s":1}]}, {"c":1, "a":3}
37+
],
38+
"key3": {
39+
"sub3": 0,
40+
"sub2": false,
41+
"sub1": "c"
42+
}
43+
}
44+
JSON;
45+
46+
$diff = new JsonDiff(json_decode($originalJson), json_decode($newJson));
47+
$expected = json_encode($diff->getPatch()->jsonSerialize(), JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES);
48+
49+
$diff = new JsonDiff(json_decode($originalJson, true), json_decode($newJson, true),
50+
JsonDiff::TOLERATE_ASSOCIATIVE_ARRAYS);
51+
$actual = json_encode($diff->getPatch()->jsonSerialize(), JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES);
52+
53+
$this->assertEquals($expected, $actual);
54+
55+
$original = json_decode($originalJson, true);
56+
$newJson = json_decode($newJson, true);
57+
$patch = JsonPatch::import(json_decode($actual, true));
58+
$patch->setFlags(JsonPatch::TOLERATE_ASSOCIATIVE_ARRAYS);
59+
$patch->apply($original);
60+
$this->assertEquals($newJson, $original);
61+
}
62+
}

0 commit comments

Comments
 (0)