|
| 1 | +<?php |
| 2 | +/** |
| 3 | + * Imported from PHPUnit\Util\Json |
| 4 | + * |
| 5 | + * We need these methods to cheaply do deep-object comparison, |
| 6 | + * (e.g. in HasJsonModelAttributes::isJsonModelAttributeDirty) |
| 7 | + * but we don't install PHPUnit in production. |
| 8 | + */ |
| 9 | + |
| 10 | +namespace Carsdotcom\JsonSchemaValidation\Helpers; |
| 11 | + |
| 12 | +use Illuminate\Support\Arr; |
| 13 | +use Illuminate\Support\Str; |
| 14 | +use InvalidArgumentException; |
| 15 | +use function count; |
| 16 | +use function is_array; |
| 17 | +use function is_object; |
| 18 | +use function json_decode; |
| 19 | +use function json_encode; |
| 20 | +use function json_last_error; |
| 21 | +use function ksort; |
| 22 | + |
| 23 | +/** |
| 24 | + * Class Json |
| 25 | + * @package App\Helpers |
| 26 | + */ |
| 27 | +class Json |
| 28 | +{ |
| 29 | + /** |
| 30 | + * Given a complex object, especially one with "magic" properties that would fail property_exists, |
| 31 | + * remove all the magic. (Like a Muggle https://en.wikipedia.org/wiki/Muggle) |
| 32 | + * turn it into a simple array or standard-object representation. |
| 33 | + * Uses JSON so arbitrary depth all gets mugglified at once. |
| 34 | + * @param $stuff |
| 35 | + * @param bool $associativeStyle Like second arg to json_decode |
| 36 | + * @return mixed |
| 37 | + */ |
| 38 | + public static function mugglify($stuff, $associativeStyle = true) |
| 39 | + { |
| 40 | + return json_decode(json_encode($stuff), $associativeStyle); |
| 41 | + } |
| 42 | + |
| 43 | + /** |
| 44 | + * To allow comparison of JSON strings, first process them into a consistent |
| 45 | + * format so that they can be compared as strings. |
| 46 | + * @param string $json |
| 47 | + * @param int $options |
| 48 | + * @return string |
| 49 | + */ |
| 50 | + public static function canonicalize(string $json, int $options = 0): string |
| 51 | + { |
| 52 | + $decodedJson = self::decodeOrThrow($json); |
| 53 | + |
| 54 | + self::recursiveSort($decodedJson); |
| 55 | + |
| 56 | + $reencodedJson = json_encode($decodedJson, $options); |
| 57 | + |
| 58 | + return $reencodedJson; |
| 59 | + } |
| 60 | + |
| 61 | + /** |
| 62 | + * Compare any two JSON serializable things, where order of string object properties is ignored. |
| 63 | + * Returns true if they are, canonically, identical |
| 64 | + * @param $thing1 |
| 65 | + * @param $thing2 |
| 66 | + * @return bool |
| 67 | + * @throws \Exception |
| 68 | + */ |
| 69 | + public static function canonicallySame($thing1, $thing2): bool |
| 70 | + { |
| 71 | + return self::canonicalize(json_encode($thing1)) === self::canonicalize(json_encode($thing2)); |
| 72 | + } |
| 73 | + |
| 74 | + /** |
| 75 | + * Compare any two JSON serializable things, where order of string object properties is ignored. |
| 76 | + * Creates an internal muggle representation without the dotted-notation keys in $ignoreKeys |
| 77 | + * Returns true if those muggles are, canonically, identical |
| 78 | + * @param $thing1 |
| 79 | + * @param $thing2 |
| 80 | + * @param array $ignoreKeys |
| 81 | + * @return bool |
| 82 | + * @throws \Exception |
| 83 | + */ |
| 84 | + public static function canonicallySameExcept($thing1, $thing2, array $ignoreKeys): bool |
| 85 | + { |
| 86 | + $muggle1 = self::mugglify($thing1); |
| 87 | + Arr::forget($muggle1, $ignoreKeys); |
| 88 | + $muggle2 = self::mugglify($thing2); |
| 89 | + Arr::forget($muggle2, $ignoreKeys); |
| 90 | + return self::canonicallySame($muggle1, $muggle2); |
| 91 | + } |
| 92 | + |
| 93 | + /* |
| 94 | + * JSON object keys are unordered while PHP array keys are ordered. |
| 95 | + * Sort all array keys to ensure both the expected and actual values have |
| 96 | + * their keys in the same order. |
| 97 | + */ |
| 98 | + private static function recursiveSort(&$json): void |
| 99 | + { |
| 100 | + if (is_array($json) === false) { |
| 101 | + // If the object is not empty, change it to an associative array |
| 102 | + // so we can sort the keys (and we will still re-encode it |
| 103 | + // correctly, since PHP encodes associative arrays as JSON objects.) |
| 104 | + // But EMPTY objects MUST remain empty objects. (Otherwise we will |
| 105 | + // re-encode it as a JSON array rather than a JSON object.) |
| 106 | + // See #2919. |
| 107 | + if (is_object($json) && count((array) $json) > 0) { |
| 108 | + $json = (array) $json; |
| 109 | + } else { |
| 110 | + return; |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + ksort($json); |
| 115 | + |
| 116 | + foreach ($json as $key => &$value) { |
| 117 | + self::recursiveSort($value); |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + /** |
| 122 | + * Wrapper for json_decode that throws when an error occurs. |
| 123 | + * Cloned here from Guzzle, to make it obvious that it behaves differently from the builtin language function. |
| 124 | + * |
| 125 | + * @param string $json JSON data to parse |
| 126 | + * @param bool $assoc When true, returned objects will be converted |
| 127 | + * into associative arrays. |
| 128 | + * @param int $depth User specified recursion depth. |
| 129 | + * @param int $options Bitmask of JSON decode options. |
| 130 | + * |
| 131 | + * @return mixed |
| 132 | + * @throws InvalidArgumentException if the JSON cannot be decoded. |
| 133 | + * @link http://www.php.net/manual/en/function.json-decode.php |
| 134 | + */ |
| 135 | + public static function decodeOrThrow($json, $assoc = false, $depth = 512, $options = 0) |
| 136 | + { |
| 137 | + $data = json_decode($json, $assoc, $depth, $options); |
| 138 | + if (JSON_ERROR_NONE !== json_last_error()) { |
| 139 | + throw new InvalidArgumentException('json_decode error: ' . json_last_error_msg()); |
| 140 | + } |
| 141 | + |
| 142 | + return $data; |
| 143 | + } |
| 144 | + |
| 145 | + public static function isObject($mixed): bool |
| 146 | + { |
| 147 | + return Str::startsWith(json_encode($mixed, flags: JSON_THROW_ON_ERROR), '{'); |
| 148 | + } |
| 149 | +} |
0 commit comments