From 58eaea457604ca59ecb0a56cdd26407d00556853 Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Wed, 24 Apr 2019 19:40:29 +0700 Subject: [PATCH] Add support for PHP associative arrays as JSON objects, resolves #17 --- .travis.yml | 1 + README.md | 4 ++- src/JsonDiff.php | 15 +++++++++ src/JsonPatch.php | 12 +++++-- src/JsonPointer.php | 40 ++++++++++++++++++---- tests/src/AssociativeTest.php | 62 +++++++++++++++++++++++++++++++++++ 6 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 tests/src/AssociativeTest.php diff --git a/.travis.yml b/.travis.yml index b6004a1..f05a89d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: php php: - nightly - hhvm + - 7.3 - 7.2 - 7.1 - 7.0 diff --git a/README.md b/README.md index b6a1322..201eab6 100644 --- a/README.md +++ b/README.md @@ -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`. @@ -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` diff --git a/src/JsonDiff.php b/src/JsonDiff.php index ae11537..9b7e0b0 100644 --- a/src/JsonDiff.php +++ b/src/JsonDiff.php @@ -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; @@ -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)) diff --git a/src/JsonPatch.php b/src/JsonPatch.php index 67d53b9..c3d6833 100644 --- a/src/JsonPatch.php +++ b/src/JsonPatch.php @@ -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; /** @@ -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: diff --git a/src/JsonPointer.php b/src/JsonPointer.php index c83c0be..d6ea833 100644 --- a/src/JsonPointer.php +++ b/src/JsonPointer.php @@ -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 @@ -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]; @@ -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)) { @@ -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; } + } diff --git a/tests/src/AssociativeTest.php b/tests/src/AssociativeTest.php new file mode 100644 index 0000000..d3a7cb6 --- /dev/null +++ b/tests/src/AssociativeTest.php @@ -0,0 +1,62 @@ +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); + } +} \ No newline at end of file