From 75715763e88d0ed253463e1cc495b69f735eba0c Mon Sep 17 00:00:00 2001 From: Adrian Date: Wed, 31 Mar 2021 22:54:26 -0400 Subject: [PATCH] version 2 --- InvalidArgumentException.php | 42 +++---- Json.php | 221 +++++++++++++++++++---------------- README.md | 63 ++++++---- 3 files changed, 182 insertions(+), 144 deletions(-) diff --git a/InvalidArgumentException.php b/InvalidArgumentException.php index b230d0b..e71208a 100644 --- a/InvalidArgumentException.php +++ b/InvalidArgumentException.php @@ -22,35 +22,35 @@ use AT\Exceptable\Spl\InvalidArgumentException as InvalidArgumentExceptable; +use const JSON_ERROR_UNSUPPORTED_TYPE; + /** * Represents invalid arguments passed to Json methods. */ class InvalidArgumentException extends InvalidArgumentExceptable { - /** @var int DECODE_ASSOC must be boolean. */ - public const INVALID_DECODE_ASSOC = 1; - - /** @var int DECODE_DEPTH must be integer. */ - public const INVALID_DECODE_DEPTH = 2; - - /** @var int DECODE_FLAGS must be integer. */ + /** + * @var int INVALID_ASSOC Option must be boolean. + * @var int INVALID_DEPTH Option must be integer. + * @var int INVALID_DECODE_FLAGS Option must be integer. + * @var int INVALID_ENCODE_FLAGS Option must be integer. + * @var int UNSUPPORTED_TYPE @see https://php.net/json.constants JSON_ERROR_UNSUPPORTED_TYPE + */ + public const INVALID_ASSOC = 1; + public const INVALID_DEPTH = 2; public const INVALID_DECODE_FLAGS = 3; - - /** @var int ENCODE_FLAGS must be integer. */ public const INVALID_ENCODE_FLAGS = 4; - - /** @var int ENCODE_DEPTH must be integer. */ - public const INVALID_ENCODE_DEPTH = 5; + public const UNSUPPORTED_TYPE = JSON_ERROR_UNSUPPORTED_TYPE; /** @see IsExceptable::getInfo() */ protected const INFO = [ - self::INVALID_DECODE_ASSOC => [ - "message" => "DECODE_ASSOC must be boolean", - "format" => "DECODE_ASSOC must be boolean; {type} provided" + self::INVALID_ASSOC => [ + "message" => "ASSOC must be boolean", + "format" => "ASSOC must be boolean; {type} provided" ], - self::INVALID_DECODE_DEPTH => [ - "message" => "DECODE_DEPTH must be integer", - "format" => "DECODE_DEPTH must be integer; {type} provided" + self::INVALID_DEPTH => [ + "message" => "DEPTH must be integer", + "format" => "DEPTH must be integer; {type} provided" ], self::INVALID_DECODE_FLAGS => [ "message" => "DECODE_FLAGS must be integer", @@ -60,9 +60,9 @@ class InvalidArgumentException extends InvalidArgumentExceptable { "message" => "ENCODE_FLAGS must be integer", "format" => "ENCODE_FLAGS must be integer; {type} provided" ], - self::INVALID_ENCODE_DEPTH => [ - "message" => "ENCODE_DEPTH must be integer", - "format" => "ENCODE_DEPTH must be integer; {type} provided" + self::UNSUPPORTED_TYPE => [ + "message" => "Type is not supported", + "format" => "Type is not supported: {type}" ] ]; } diff --git a/Json.php b/Json.php index 6ae3100..3bde850 100644 --- a/Json.php +++ b/Json.php @@ -26,11 +26,42 @@ use AT\Simple\Json\InvalidArgumentException; +use const JSON_ERROR_UNSUPPORTED_TYPE; + /** * Convenience wrapper for json encoding/decoding. */ class Json { + /** + * Keys for $options tuples. + * + * @type int ASSOC Decode objects as arrays? + * @type int DEPTH Maximum recursion level + * @type int DECODE_FLAGS Decoding options + * @type int ENCODE_FLAGS Encoding options + */ + public const ASSOC = 0; + public const DEPTH = 1; + public const DECODE_FLAGS = 2; + public const ENCODE_FLAGS = 3; + + /** + * Default encode and decode options. + * + * @type bool DEFAULT_ASSOC Prefer decoding data as arrays + * @type int DEFAULT_DEPTH Default depth + * @type int DEFAULT_DECODE_FLAGS Preferred options for json_decode + * @type int DEFAULT_ENCODE_FLAGS Preferred options for json_encode + */ + protected const DEFAULT_ASSOC = true; + protected const DEFAULT_DEPTH = 512; + protected const DEFAULT_DECODE_FLAGS = JSON_BIGINT_AS_STRING; + protected const DEFAULT_ENCODE_FLAGS = JSON_BIGINT_AS_STRING | + JSON_PRESERVE_ZERO_FRACTION | + JSON_UNESCAPED_SLASHES | + JSON_UNESCAPED_UNICODE; + /** * Encode options. * @@ -45,164 +76,152 @@ class Json { public const ENCODE_PRETTY = self::DEFAULT_ENCODE_FLAGS | JSON_PRETTY_PRINT; /** - * Keys for decode/encode $options tuples. + * Factory: convenience method for building a new Json instance with "ascii" options. * - * @type int DECODE_ASSOC Decode objects as arrays? - * @type int DECODE_DEPTH Maximum recursion level to decode - * @type int DECODE_FLAGS Decoding options - * @type int ENCODE_FLAGS Encoding options - * @type int ENCODE_DEPTH Maximum recursion level to encode + * @return Json */ - public const DECODE_ASSOC = 0; - public const DECODE_DEPTH = 1; - public const DECODE_FLAGS = 2; - public const ENCODE_FLAGS = 0; - public const ENCODE_DEPTH = 1; + public static function ascii() : Json { + return new self([self::ENCODE_FLAGS => self::ENCODE_ASCII]); + } - /** @var int Error code: Json is a string encoding format. */ - public const JSON_MUST_BE_STRING = 66; + /** + * Factory: convenience method for building a new Json instance with default options. + * + * @return Json + */ + public static function default() : Json { + return new self(); + } /** - * Default encode and decode options. + * Factory: convenience method for building a new Json instance with "hex" options. * - * @type bool DEFAULT_ASSOC Prefer decoding data as arrays - * @type int DEFAULT_DECODE_FLAGS Preferred options for json_decode - * @type int DEFAULT_ENCODE_FLAGS Preferred options for json_encode - * @type int DEFAULT_DEPTH Default depth + * @return Json */ - protected const DEFAULT_ASSOC = true; - protected const DEFAULT_DECODE_FLAGS = JSON_BIGINT_AS_STRING; - protected const DEFAULT_ENCODE_FLAGS = JSON_BIGINT_AS_STRING | - JSON_PRESERVE_ZERO_FRACTION | - JSON_UNESCAPED_SLASHES | - JSON_UNESCAPED_UNICODE; - protected const DEFAULT_DEPTH = 512; + public static function hex() : Json { + return new self([self::ENCODE_FLAGS => self::ENCODE_HEX]); + } /** - * Decodes a Json string. + * Factory: convenience method for building a new Json instance with "html" options. * - * @param string $json The json string to decode - * @param array $options Options for decoding: { - * @var bool ${self::DECODE_ASSOC} @see https://.php.net/json_decode $assoc - * @var int ${self::DECODE_DEPTH} @see https://.php.net/json_decode $depth - * @var int ${self::DECODE_FLAGS} @see https://.php.net/json_decode $options - * } - * @throws InvalidArgumentException INVALID_DECODE_ASSOC if DECODE_ASSOC is not bool - * @throws InvalidArgumentException INVALID_DECODE_DEPTH if DECODE_DEPTH is not an int - * @throws InvalidArgumentException INVALID_DECODE_FLAGS if DECODE_FLAGS is not an int - * @throws JsonException If decoding fails - * @return mixed The decoded data on success + * @return Json */ - public static function decode(string $json, array $options = []) { - return json_decode($json, ...self::parseDecodeOptions($options)); + public static function html() : Json { + return new self([self::ENCODE_FLAGS => self::ENCODE_HTML]); } /** - * Encodes a value as Json. + * Factory: convenience method for building a new Json instance with "pretty" options. * - * @param mixed $data Data to encode - * @param array $options Options for encoding: { - * @var int ${self::ENCODE_FLAGS} @see https://.php.net/json_encode $options - * @var int ${self::ENCODE_DEPTH} @see https://.php.net/json_encode $depth - * } - * @throws InvalidArgumentException INVALID_ENCODE_FLAGS if ENCODE_FLAGS is not an int - * @throws InvalidArgumentException INVALID_ENCODE_DEPTH if ENCODE_DEPTH is not an int - * @throws JsonException If encoding fails - * @return string The encoded json string on success + * @return Json */ - public static function encode($data, array $options = []) : string { - return json_encode($data, ...self::parseEncodeOptions($options)); + public static function pretty() : Json { + return new self([self::ENCODE_FLAGS => self::ENCODE_PRETTY]); } /** - * Can the given value be encoded as json? + * Parsed encode/decode options. + */ + protected $assoc; + protected $depth; + protected $decodeFlags; + protected $encodeFlags; + + /** + * @param array $options @see setOptions() + */ + public function __construct(array $options = []) { + $this->setOptions($options); + } + + /** + * Decodes a Json string. * - * Note; this method considers objects "encodable" only if they are stdClass or JsonSerializable. + * @param string $json The json string to decode + * @throws JsonException If decoding fails + * @return mixed The decoded data on success */ - public static function isJsonable($value) : bool { - return is_object($value) ? - ($value instanceof stdClass || $value instanceof JsonSerializable) : - ! is_resource($value); + public static function decode(string $json) { + return json_decode($json, $this->assoc, $this->depth, $this->decodeFlags); } /** - * Is the given value a valid json string? + * Encodes a value as Json. + * + * Note, objects are considered "encodable" only if they are stdClass or JsonSerializable. + * Pass $strict = false to override this. * - * @param mixed $value The value to check - * @param Throwable|null &$error Filled if json is invalid; null otherwise - * @return bool True if value is valid json; false otherwise + * @param mixed $data Data to encode + * @param bool $strict Don't encode non-json-able objects? + * @throws JsonException If encoding fails + * @return string The encoded json string on success */ - public static function isValid($value, &$error = null) : bool { - $error = null; - - try { - self::decode($value); - return true; - } catch (TypeError | JsonException $e) { - $error = $e; - return false; + public static function encode($data, bool $strict = true) : string { + if ( + is_object($data) && + ! ($data instanceof stdClass || $data instanceof JsonSerializable) + ) { + $e = new InvalidArgumentException( + InvalidArgumentException::UNSUPPORTED_TYPE, + ['type' => get_class($data)] + ); + throw new JsonException($e->getMessage(), JSON_ERROR_UNSUPPORTED_TYPE, $e); } + + return json_encode($data, $this->encodeFlags, $this->depth); } /** - * Parses decode options. + * Sets encode/decode options. * - * @param array $options Options to parse - * @throws InvalidArgumentException If any options are invalid; @see Json::decode() $options - * @return array Options tuple: [assoc, depth, flags] + * @param array $options Options to parse: { + * @var bool ${self::ASSOC} @see https://php.net/json_decode $assoc + * @var int ${self::DEPTH} @see https://php.net/json_decode $depth + * @var int ${self::DECODE_FLAGS} @see https://php.net/json_decode $options + * @var int ${self::ENCODE_FLAGS} @see https://php.net/json_encode $options + * } + * @throws InvalidArgumentException If any options are invalid + * @return Json $this */ - protected static function parseDecodeOptions(array $options) : array { + public function setOptions(array $options) : Json { $assoc = $options[self::DECODE_ASSOC] ?? self::DEFAULT_ASSOC; if (! is_bool($assoc)) { InvalidArgumentException::throw( - InvalidArgumentException::INVALID_DECODE_ASSOC, + InvalidArgumentException::INVALID_ASSOC, ["type" => gettype($assoc)] ); } $depth = $options[self::DECODE_DEPTH] ?? self::DEFAULT_DEPTH; - if (! is_int($depth)) { + if (! is_int($depth) || $depth < 0) { InvalidArgumentException::throw( - InvalidArgumentException::INVALID_DECODE_DEPTH, + InvalidArgumentException::INVALID_DEPTH, ["type" => gettype($depth)] ); } - $flags = $options[self::DECODE_FLAGS] ?? self::DEFAULT_DECODE_FLAGS; - if (! is_int($flags)) { + $decodeFlags = $options[self::DECODE_FLAGS] ?? self::DEFAULT_DECODE_FLAGS; + if (! is_int($flags) || $depth < 0) { InvalidArgumentException::throw( InvalidArgumentException::INVALID_DECODE_FLAGS, ["type" => gettype($flags)] ); } - return [$assoc, $depth, $flags | JSON_THROW_ON_ERROR]; - } - - /** - * Parses encode options. - * - * @param array $options Options to parse - * @throws InvalidArgumentException If any options are invalid; @see Json::encode() $options - * @return array Options tuple: [flags, depth] - */ - protected static function parseEncodeOptions(array $options) : array { - $flags = $options[self::ENCODE_FLAGS] ?? self::DEFAULT_ENCODE_FLAGS; - if (! is_int($flags)) { + $encodeFlags = $options[self::ENCODE_FLAGS] ?? self::DEFAULT_ENCODE_FLAGS; + if (! is_int($flags) || $depth < 0) { InvalidArgumentException::throw( InvalidArgumentException::INVALID_ENCODE_FLAGS, ["type" => gettype($flags)] ); } - $depth = $options[self::ENCODE_DEPTH] ?? self::DEFAULT_DEPTH; - if (! is_int($depth)) { - InvalidArgumentException::throw( - InvalidArgumentException::INVALID_ENCODE_DEPTH, - ["type" => gettype($depth)] - ); - } + $this->assoc = $assoc; + $this->depth = $depth; + $this->decodeFlags = $decodeFlags | JSON_THROW_ON_ERROR; + $this->encodeFlags = $encodeFlags | JSON_THROW_ON_ERROR; - return [$flags | JSON_THROW_ON_ERROR, $depth]; + return $this; } } diff --git a/README.md b/README.md index 2ec270c..629923e 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ it's simple: json ================= -_Simple_ packages are focused on being straightforward and unopinionated solutions for common needs. +_Simple_ packages are focused on being straightforward, clean, concise solutions for common needs. -_Json_ is a convenience wrapper for json encoding/decoding. Its main purpose is to make managing encoding/decoding options easy. +_Json_ is a convenience wrapper for json encoding/decoding. Its main purpose is to set sane defaults and make managing encoding/decoding options easy. dependencies ------------ @@ -19,19 +19,33 @@ Recommended installation method is via [Composer](https://getcomposer.org/): sim basic usage ----------- +By default, objects are decoded as associative arrays and big integers are decoded as strings (rather than converting them to float). + +When encoding data, big integers are encoded as strings, "zero" fractions are preserved (rather then encoding them as integers), and slashes and unicode characters are not escaped. To prevent unexpected/unpredictable results, objects will not be encoded unless they are `stdClass` or implement `JsonSerializable`. + +Both encoding and decoding throw on error; this cannot be overridden. + +Json defines some constants for sets of encode/decode options. As a convenience, these options are also settable via static factory methods. +- `ENCODE_ASCII`: default encoding options, but unsets `JSON_UNESCAPED_UNICODE`. +- `ENCODE_HEX`: all of the `JSON_HEX_*` options. +- `ENCODE_HTML`: default encoding options, but unsets `JSON_UNESCAPED_SLASHES`. +- `ENCODE_PRETTY`: default encoding options, and also sets `JSON_PRETTY_PRINT`. + ```php "one", "bar" => "two"]; // basic encoding and decoding (note $assoc = true is the default mode) -$json = Json::encode($a); // {"foo":"one","bar":"two"} -var_dump($a === Json::decode()); // bool (true) +$json = Json::default(); +$j = $json->encode($a); // {"foo":"one","bar":"two"} +$a === $json->decode($j); // bool (true) -// passing special options -Json::encode($a, [Json::ENCODE_FLAGS => Json::ENCODE_PRETTY]); +// special options - e.g., "pretty" formatting +Json::pretty()->encode($a); /* { "foo": "one", @@ -39,7 +53,8 @@ Json::encode($a, [Json::ENCODE_FLAGS => Json::ENCODE_PRETTY]); } */ -var_dump(Json::decode($json, [Json::DECODE_ASOOC => false])); +// decoding objects as stdClass +(new Json([Json::DECODE_ASOOC => false]))->decode($j); /* object(stdClass)#1 (2) { ["foo"]=> @@ -48,26 +63,30 @@ object(stdClass)#1 (2) { string(3) "two" } */ - -var_dump(Json::isValid('["oh, foo"', $e)); // bool (false) -echo $e->getMessage(); // Syntax error ``` -By default, objects are decoded as associative arrays and big integers are decoded as strings (rather than converting them to float). -When encoding data, big integers are encoded as strings, "zero" fractions are preserved (rather then encoding them as integers), and slashes and unicode characters are not escaped. - -Both encoding and decoding throw on error; this cannot be overridden. - -Json defines some convenience constants for sets of encode/decode options: -- `ENCODE_ASCII`: default encoding options, but unsets `JSON_UNESCAPED_UNICODE`. -- `ENCODE_HEX`: all of the `JSON_HEX_*` options. -- `ENCODE_HTML`: default encoding options, but unsets `JSON_UNESCAPED_SLASHES`. -- `ENCODE_PRETTY`: default encoding options, and also sets `JSON_PRETTY_PRINT`. - docs ---- -_coming soon_ +- **[Getting Started (It's Simple)](/php-enspired/simple-json/wiki/It's-Simple:-Json)** + - [dependencies](https://github.com/php-enspired/simple-json/wiki/It's-Simple:-Json#dependencies) + - [installation](https://github.com/php-enspired/simple-json/wiki/It's-Simple:-Json#installation) + - [basic usage](https://github.com/php-enspired/simple-json/wiki/It's-Simple:-Json#basic-usage) +- **[Constructor and Factory Methods](/php-enspired/simple-json/wiki/Constructor-and-Factory-Methods)** + - [`__construct()`](/php-enspired/simple-json/wiki/Constructor-and-Factory-Methods#__construct-array-options---) + - [`::default()`](/php-enspired/simple-json/wiki/Constructor-and-Factory-Methods#static-default-void---json) + - [`::ascii()`](/php-enspired/simple-json/wiki/Constructor-and-Factory-Methods#static-ascii-void---json) + - [`::hex()`](/php-enspired/simple-json/wiki/Constructor-and-Factory-Methods#static-hex-void---json) + - [`::html()`](/php-enspired/simple-json/wiki/Constructor-and-Factory-Methods#static-html-void---json) + - [`::pretty()`](/php-enspired/simple-json/wiki/Constructor-and-Factory-Methods#static-pretty-void---json) +- **[Encoding and Decoding](/php-enspired/simple-json/wiki/Encoding-and-Decoding)** + - [`encode()`](/php-enspired/simple-json/wiki/Encoding-and-Decoding#encode-mixed-data--bool-strict--true---string) + - [`decode()`](/php-enspired/simple-json/wiki/Encoding-and-Decoding#decode-string-json---mixed) +- **[Managing Options](/php-enspired/simple-json/wiki/Managing-Options)** + - [`setOptions()`](/php-enspired/simple-json/wiki/Managing-Options#setoptions-array-options---json) +- **[Handling Errors](/php-enspired/simple-json/wiki/Handling-Errors)** + - [`JsonException`](/php-enspired/simple-json/wiki/Handling-Errors#jsonexception) + - [`InvalidArgumentException`](/php-enspired/simple-json/wiki/Handling-Errors#invalidargumentexception) contributing or getting help ----------------------------