"replace", "path" => "$path", "value" => $other)); } } return array(); } // Walk associative arrays $src and $dst, returning a list of patches private static function diff_assoc($path, $src, $dst) { $result = array(); if (count($src) == 0 && count($dst) != 0) { $result[] = array("op" => "replace", "path" => "$path", "value" => $dst); } else { foreach (array_keys($src) as $key) { $ekey = self::escape_pointer_part($key); if (!array_key_exists($key, $dst)) { $result[] = array("op" => "remove", "path" => "$path/$ekey"); } else { $result = array_merge($result, self::diff_values("$path/$ekey", $src[$key], $dst[$key])); } } foreach (array_keys($dst) as $key) { if (!array_key_exists($key, $src)) { $ekey = self::escape_pointer_part($key); $result[] = array("op" => "add", "path" => "$path/$ekey", "value" => $dst[$key]); } } } return $result; } // Walk simple arrays $src and $dst, returning a list of patches private static function diff_array($path, $src, $dst) { $result = array(); $lsrc = count($src); $ldst = count($dst); $max = ($lsrc > $ldst) ? $lsrc : $ldst; // Walk backwards through arrays, starting with longest $i = $max - 1; while ($i >= 0) // equivalent for loop didn't work? { if ($i < $lsrc && $i < $ldst && array_key_exists($i, $src) && array_key_exists($i, $dst)) { $result = array_merge($result, self::diff_values("$path/$i", $src[$i], $dst[$i])); } else if ($i < $ldst && array_key_exists($i, $dst)) { $result[] = array("op" => "add", "path" => "$path/$i", "value" => $dst[$i]); } else if ($i < $lsrc && !array_key_exists($i, $dst)) { $result[] = array("op" => "remove", "path" => "$path/$i"); } $i--; } return $result; } // patch support functions // Implements the 'test' op private static function test($doc, $path, $parts, $value, $simplexml_mode) { $found = self::get_helper($doc, $path, $parts, $simplexml_mode); if (!self::considered_equal($found, $value)) { throw new JsonPatchException("test target value different - expected " . json_encode($value) . ", found " . json_encode($found)); } } // Helper for get() and 'copy', 'move', 'test' ops - get a value from a doc. private static function get_helper($doc, $path, $parts, $simplexml_mode) { if (count($parts) == 0) { return $doc; } $part = array_shift($parts); if (!is_array($doc) || !array_key_exists($part, $doc)) { throw new JsonPatchException("Path '$path' not found"); } if ($simplexml_mode && count($parts) > 0 && $parts[0] == '0' && self::is_associative($doc) && !(is_array($doc[$part]) && !self::is_associative($doc[$part]))) { return self::get_helper(array($doc[$part]), $path, $parts, $simplexml_mode); } else { return self::get_helper($doc[$part], $path, $parts, $simplexml_mode); } } // Test whether a php array looks 'associative' - does it have // any non-numeric keys? // // note: is_associative(array()) === false private static function is_associative($a) { if (!is_array($a)) { return false; } foreach (array_keys($a) as $key) { if (is_string($key)) { return true; } } // Also treat php gappy arrays as associative. // (e.g. {"0":"a", "2":"c"}) $len = count($a); for ($i = 0; $i < $len; $i++) { if (!array_key_exists($i, $a)) { return true; } } return false; } // Recursively sort array keys private static function rksort($a) { if (!is_array($a)) { return $a; } foreach (array_keys($a) as $key) { $a[$key] = self::rksort($a[$key]); } // SORT_STRING seems required, as otherwise numeric indices // (e.g. "4") aren't sorted. ksort($a, SORT_STRING); return $a; } // Per http://tools.ietf.org/html/rfc6902#section-4.6 public static function considered_equal($a1, $a2) { return json_encode(self::rksort($a1)) === json_encode(self::rksort($a2)); } // Apply a single op to modify the given document. // // As php arrays are not passed by reference, this function works // recursively, rebuilding complete subarrays that need changing; // the revised subarray is changed in the parent array before // returning it. private static function do_op($doc, $op, $path, $parts, $value, $simplexml_mode) { // Special-case toplevel if (count($parts) == 0) { if ($op == 'add' || $op == 'replace') { return $value; } else if ($op == 'remove') { throw new JsonPatchException("Can't remove whole document"); } else { throw new JsonPatchException("'$op' can't operate on whole document"); } } $part = array_shift($parts); // recur until we get to the target if (count($parts) > 0) { if (!array_key_exists($part, $doc)) { throw new JsonPatchException("Path '$path' not found"); } // recur, adding resulting sub-doc into doc returned to caller // special case for simplexml-style behavior - make singleton // scalar leaves look like 1-length arrays if ($simplexml_mode && count($parts) > 0 && ($parts[0] == '0' || $parts[0] == '1' || $parts[0] == '-') && self::is_associative($doc) && !(is_array($doc[$part]) && !self::is_associative($doc[$part]))) { $doc[$part] = self::do_op(array($doc[$part]), $op, $path, $parts, $value, $simplexml_mode); } else { $doc[$part] = self::do_op($doc[$part], $op, $path, $parts, $value, $simplexml_mode); } return $doc; } // at target if (!is_array($doc)) { throw new JsonPatchException('Target must be array or associative array'); } if (!self::is_associative($doc)) // N.B. returns false for empty arrays { if (count($doc) && !self::is_index($part) && !($part == '-' && ($op == 'add' || $op == 'append'))) { throw new JsonPatchException("Non-array key '$part' used on array"); } else { // check range, if numeric if (self::is_index($part) && ($part < 0 || (($op == 'remove' && $part >= count($doc)) || ($op != 'remove' && $part > count($doc))))) { throw new JsonPatchException("Can't operate outside of array bounds"); } } } if ($op == 'add' || $op == 'append') { if (!self::is_associative($doc) && (self::is_index($part) || $part == '-')) { // If index is '-', use array length $index = ($part == '-') ? count($doc) : $part; if ($op == 'append') { array_splice($doc, $index, 0, $value); } else { array_splice($doc, $index, 0, Array($value)); } } else { $doc[$part] = $value; } } else if ($op == 'replace') { if (!self::is_associative($doc) && self::is_index($part)) { array_splice($doc, $part, 1, Array($value)); } else { if (!array_key_exists($part, $doc)) { throw new JsonPatchException("replace target '$path' not set"); } $doc[$part] = $value; } } else if ($op == 'remove') { if (!self::is_associative($doc) && self::is_index($part)) { array_splice($doc, $part, 1); } else { if (!array_key_exists($part, $doc)) { throw new JsonPatchException("remove target '$path' not set"); } unset($doc[$part]); } } return $doc; } }