Skip to content

Commit

Permalink
Merge pull request #21 from swaggest/assoc-array
Browse files Browse the repository at this point in the history
Add support for PHP associative arrays as JSON objects, resolves #17
  • Loading branch information
vearutop authored Apr 24, 2019
2 parents 1282c40 + 58eaea4 commit de18f54
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 11 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ language: php
php:
- nightly
- hhvm
- 7.3
- 7.2
- 7.1
- 7.0
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Available options:
* `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").
* `SKIP_JSON_PATCH` is an option to improve performance by not building JsonPatch for this diff.
* `SKIP_JSON_MERGE_PATCH` is an option to improve performance by not building JSON Merge Patch value for this diff.
* `TOLERATE_ASSOCIATIVE_ARRAYS` is an option to allow associative arrays to mimic JSON objects (not recommended).

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

Expand Down Expand Up @@ -123,9 +124,10 @@ Applies patch to `JSON`-decoded data.
#### `setFlags`
Alters default behavior.

Available flag:
Available flags:

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

### `JsonPointer`

Expand Down
15 changes: 15 additions & 0 deletions src/JsonDiff.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ class JsonDiff
*/
const SKIP_JSON_MERGE_PATCH = 16;

/**
* TOLERATE_ASSOCIATIVE_ARRAYS is an option to allow associative arrays to mimic JSON objects (not recommended)
*/
const TOLERATE_ASSOCIATIVE_ARRAYS = 32;

private $options = 0;
private $original;
private $new;
Expand Down Expand Up @@ -236,6 +241,16 @@ private function process($original, $new)
{
$merge = !($this->options & self::SKIP_JSON_MERGE_PATCH);

if ($this->options & self::TOLERATE_ASSOCIATIVE_ARRAYS) {
if (is_array($original) && !empty($original) && !array_key_exists(0, $original)) {
$original = (object)$original;
}

if (is_array($new) && !empty($new) && !array_key_exists(0, $new)) {
$new = (object)$new;
}
}

if (
(!$original instanceof \stdClass && !is_array($original))
|| (!$new instanceof \stdClass && !is_array($new))
Expand Down
12 changes: 9 additions & 3 deletions src/JsonPatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ class JsonPatch implements \JsonSerializable
*/
const STRICT_MODE = 2;

/**
* Allow associative arrays to mimic JSON objects (not recommended)
*/
const TOLERATE_ASSOCIATIVE_ARRAYS = 8;


private $flags = 0;

/**
Expand Down Expand Up @@ -146,15 +152,15 @@ public function apply(&$original, $stopOnError = true)
case $operation instanceof Move:
$fromItems = JsonPointer::splitPath($operation->from);
$value = JsonPointer::get($original, $fromItems);
JsonPointer::remove($original, $fromItems);
JsonPointer::remove($original, $fromItems, $this->flags);
JsonPointer::add($original, $pathItems, $value, $this->flags);
break;
case $operation instanceof Remove:
JsonPointer::remove($original, $pathItems);
JsonPointer::remove($original, $pathItems, $this->flags);
break;
case $operation instanceof Replace:
JsonPointer::get($original, $pathItems);
JsonPointer::remove($original, $pathItems);
JsonPointer::remove($original, $pathItems, $this->flags);
JsonPointer::add($original, $pathItems, $operation->value, $this->flags);
break;
case $operation instanceof Test:
Expand Down
40 changes: 33 additions & 7 deletions src/JsonPointer.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ class JsonPointer
*/
const SKIP_IF_ISSET = 4;

/**
* Allow associative arrays to mimic JSON objects (not recommended)
*/
const TOLERATE_ASSOCIATIVE_ARRAYS = 8;

/**
* @param string $key
* @param bool $isURIFragmentId
Expand Down Expand Up @@ -135,12 +140,18 @@ public static function add(&$holder, $pathItems, $value, $flags = self::RECURSIV
array_splice($ref, $key, 0, array($value));
}
if (false === $intKey) {
throw new Exception('Invalid key for array operation');
if (0 === ($flags & self::TOLERATE_ASSOCIATIVE_ARRAYS)) {
throw new Exception('Invalid key for array operation');
}
$ref = &$ref[$key];
continue;
}
if ($intKey > count($ref) && 0 === ($flags & self::RECURSIVE_KEY_CREATION)) {
throw new Exception('Index is greater than number of items in array');
} elseif ($intKey < 0) {
throw new Exception('Negative index');
if (0 === ($flags & self::TOLERATE_ASSOCIATIVE_ARRAYS)) {
if ($intKey > count($ref) && 0 === ($flags & self::RECURSIVE_KEY_CREATION)) {
throw new Exception('Index is greater than number of items in array');
} elseif ($intKey < 0) {
throw new Exception('Negative index');
}
}

$ref = &$ref[$intKey];
Expand Down Expand Up @@ -235,10 +246,11 @@ public static function getByPointer($holder, $pointer)
/**
* @param mixed $holder
* @param string[] $pathItems
* @param int $flags
* @return mixed
* @throws Exception
*/
public static function remove(&$holder, $pathItems)
public static function remove(&$holder, $pathItems, $flags = 0)
{
$ref = &$holder;
while (null !== $key = array_shift($pathItems)) {
Expand Down Expand Up @@ -269,12 +281,26 @@ public static function remove(&$holder, $pathItems)
if ($parent instanceof \stdClass || is_object($parent)) {
unset($parent->$refKey);
} else {
$isAssociative = false;
$ff = $flags & self::TOLERATE_ASSOCIATIVE_ARRAYS;
if ($flags & self::TOLERATE_ASSOCIATIVE_ARRAYS) {
$i = 0;
foreach ($parent as $index => $value) {
if ($i !== $index) {
$isAssociative = true;
break;
}
}
}

unset($parent[$refKey]);
if ($refKey !== count($parent)) {
if (!$isAssociative && (int)$refKey !== count($parent)) {
$parent = array_values($parent);
}
}
}

return $ref;
}

}
62 changes: 62 additions & 0 deletions tests/src/AssociativeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace Swaggest\JsonDiff\Tests;


use Swaggest\JsonDiff\JsonDiff;
use Swaggest\JsonDiff\JsonPatch;

class AssociativeTest extends \PHPUnit_Framework_TestCase
{
/**
* @throws \Swaggest\JsonDiff\Exception
*/
public function testDiffAssociative()
{
$originalJson = <<<'JSON'
{
"key1": [4, 1, 2, 3],
"key2": 2,
"key3": {
"sub0": 0,
"sub1": "a",
"sub2": "b"
},
"key4": [
{"a":1, "b":true, "subs": [{"s":1}, {"s":2}, {"s":3}]}, {"a":2, "b":false}, {"a":3}
]
}
JSON;

$newJson = <<<'JSON'
{
"key5": "wat",
"key1": [5, 1, 2, 3],
"key4": [
{"c":false, "a":2}, {"a":1, "b":true, "subs": [{"s":3, "add": true}, {"s":2}, {"s":1}]}, {"c":1, "a":3}
],
"key3": {
"sub3": 0,
"sub2": false,
"sub1": "c"
}
}
JSON;

$diff = new JsonDiff(json_decode($originalJson), json_decode($newJson));
$expected = json_encode($diff->getPatch()->jsonSerialize(), JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES);

$diff = new JsonDiff(json_decode($originalJson, true), json_decode($newJson, true),
JsonDiff::TOLERATE_ASSOCIATIVE_ARRAYS);
$actual = json_encode($diff->getPatch()->jsonSerialize(), JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES);

$this->assertEquals($expected, $actual);

$original = json_decode($originalJson, true);
$newJson = json_decode($newJson, true);
$patch = JsonPatch::import(json_decode($actual, true));
$patch->setFlags(JsonPatch::TOLERATE_ASSOCIATIVE_ARRAYS);
$patch->apply($original);
$this->assertEquals($newJson, $original);
}
}

0 comments on commit de18f54

Please sign in to comment.