From 67fc977d2921516d53cdbe026b63ccdf8e8c5d61 Mon Sep 17 00:00:00 2001 From: John Whitley Date: Mon, 23 Sep 2019 07:42:29 +0000 Subject: [PATCH 1/2] Add "forceObject" option to the encoder. This commit adds the new "forceObject" flag to be passed to the encoder. The implementation assumes all nested arrays to be forced to be objects. --- CHANGELOG.md | 2 +- src/Encoder.php | 14 ++++ src/Json.php | 12 +++- test/JsonTest.php | 174 +++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 197 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd1180eb..9d66c03c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,7 @@ All notable changes to this project will be documented in this file, in reverse ### Added -- Nothing. +- forceObject flag to the encoder. ### Changed diff --git a/src/Encoder.php b/src/Encoder.php index 4d0620191..db28be2f8 100644 --- a/src/Encoder.php +++ b/src/Encoder.php @@ -69,6 +69,17 @@ public static function encode($value, $cycleCheck = false, array $options = []) return $encoder->encodeValue($value); } + /** + * Discover whether the force-object option flag is set, which would + * imply this encoder should force arrays to be objects. + * + * @return bool the encode should force arrays to be objects. + */ + protected function isForceObjectSet() + { + return isset($this->options['forceObject']) && $this->options['forceObject']; + } + /** * Encode a value to JSON. * @@ -189,6 +200,9 @@ protected function wasVisited(&$value) */ protected function encodeArray($array) { + if ($this->isForceObjectSet()) { + return $this->encodeAssociativeArray($array); + } // Check for associative array if (! empty($array) && (array_keys($array) !== range(0, count($array) - 1))) { // Associative array diff --git a/src/Json.php b/src/Json.php index 38822b883..7c1439ed5 100644 --- a/src/Json.php +++ b/src/Json.php @@ -293,7 +293,8 @@ private static function decodeViaPhpBuiltIn($encodedValue, $objectDecodeType) private static function encodeValue($valueToEncode, $cycleCheck, array $options, $prettyPrint) { if (function_exists('json_encode') && static::$useBuiltinEncoderDecoder !== true) { - return self::encodeViaPhpBuiltIn($valueToEncode, $prettyPrint); + $forceObject = (isset($options['forceObject']) && ($options['forceObject'] === true)); + return self::encodeViaPhpBuiltIn($valueToEncode, $prettyPrint, $forceObject); } return self::encodeViaEncoder($valueToEncode, $cycleCheck, $options, $prettyPrint); @@ -311,12 +312,15 @@ private static function encodeValue($valueToEncode, $cycleCheck, array $options, * * If $prettyPrint is boolean true, also uses JSON_PRETTY_PRINT. * + * If $forceObject is boolean true, also uses JSON_FORCE_OBJECT. + * * @param mixed $valueToEncode * @param bool $prettyPrint + * @param bool $forceObject * @return string|false Boolean false return value if json_encode is not * available, or the $useBuiltinEncoderDecoder flag is enabled. */ - private static function encodeViaPhpBuiltIn($valueToEncode, $prettyPrint = false) + private static function encodeViaPhpBuiltIn($valueToEncode, $prettyPrint = false, $forceObject = false) { if (! function_exists('json_encode') || static::$useBuiltinEncoderDecoder === true) { return false; @@ -328,6 +332,10 @@ private static function encodeViaPhpBuiltIn($valueToEncode, $prettyPrint = false $encodeOptions |= JSON_PRETTY_PRINT; } + if ($forceObject) { + $encodeOptions |= JSON_FORCE_OBJECT; + } + return json_encode($valueToEncode, $encodeOptions); } diff --git a/test/JsonTest.php b/test/JsonTest.php index d2281bd01..515caf0e4 100644 --- a/test/JsonTest.php +++ b/test/JsonTest.php @@ -1010,8 +1010,8 @@ public function testPrettyPrintEmptyPropertiesWithWhitespace() ], "bar": { - - + + } } JSON; @@ -1142,4 +1142,174 @@ public function testWillDecodeStructureWithEmptyKeyToObjectProperly() $object = Json\Json::decode($json, Json\Json::TYPE_OBJECT); $this->assertAttributeEquals('test', '_empty_', $object); } + + /** + * Get consistent input data to test the four possible encoding paths: + * encoder: BuiltIn , forceObject: false + * encoder: BuiltIn , forceObject: true + * encoder: Encoder , forceObject: false + * encoder: Encoder , forceObject: true + * + * @see getExpectedForceObjectTestResult + * @see getExpectedNotForcedObjectTestResult + * @see testForceObjectWithBuiltInEncoder + * @see testForceObjectWithEncoderComponent + * @see testNotForcedObjectWithBuiltInEncoder + * @see testNotForcedObjectWithEncoderComponent + * @return array known consistent forceObject test data + */ + private function getForceObjectTestData() + { + $source = [ + 0 => 'zero', + 1 => 'one', + 2 => 'two', + 3 => 'three', + 4 => [ + 100 => 'one hundred', + 200 => 'two hundred', + 300 => 'three hundred', + ], + 5 => [ + 'a','b','c','d','e', + ], + 6 => [ + 0,1,2,3,4,5, + ], + 7 => [], + 8 => [ + "a" => [1], + ], + ]; + + return $source; + } + + /** + * Get expected output of + * encoder: BuiltIn , forceObject: true + * encoder: Encoder , forceObject: true + * + * @see getForceObjectTestData + * @see testForceObjectWithBuiltInEncoder + * @see testForceObjectWithEncoderComponent + * @return string expected JSON encoded string of the data from {@see JsonTest::getForceObjectTestData()}. + */ + private function getExpectedForceObjectTestResult() + { + $expected = '{"0":"zero","1":"one","2":"two","3":"three",' + . '"4":{"100":"one hundred","200":"two hundred","300":"three hundred"},' + . '"5":{"0":"a","1":"b","2":"c","3":"d","4":"e"},' + . '"6":{"0":0,"1":1,"2":2,"3":3,"4":4,"5":5},"7":{},"8":{"a":{"0":1}}}'; + + return $expected; + } + + /** + * Get expected output of + * encoder: BuiltIn , forceObject: false + * encoder: Encoder , forceObject: false + * + * @see getForceObjectTestData + * @see testNotForcedObjectWithBuiltInEncoder + * @see testNotForcedObjectWithEncoderComponent + * @return string expected JSON encoded string of the data from {@see JsonTest::getForceObjectTestData()}. + */ + private function getExpectedNotForcedObjectTestResult() + { + $expected = '["zero","one","two","three",{"100":"one hundred","200":"two hundred","300":"three hundred"},' + . '["a","b","c","d","e"],[0,1,2,3,4,5],[],{"a":[1]}]'; + + return $expected; + } + + /** + * Test built in encoder when setting the forceObject flag. + */ + public function testForceObjectWithBuiltInEncoder() + { + Json\Json::$useBuiltinEncoderDecoder = true; + + $source = $this->getForceObjectTestData(); + $expected = $this->getExpectedForceObjectTestResult(); + + $actual = Json\Json::encode($source, false, ["forceObject" => true]); + + $this->assertEquals($expected, $actual); + } + + /** + * Test Encoder class when setting the forceObject flag. + */ + public function testForceObjectWithEncoderComponent() + { + Json\Json::$useBuiltinEncoderDecoder = false; + + $source = $this->getForceObjectTestData(); + $expected = $this->getExpectedForceObjectTestResult(); + + $actual = Json\Json::encode($source, false, ["forceObject" => true]); + + $this->assertEquals($expected, $actual); + } + + /** + * Test built in encoder when clearing the forceObject flag. + */ + public function testNotForcedObjectWithBuiltInEncoder() + { + Json\Json::$useBuiltinEncoderDecoder = true; + + $source = $this->getForceObjectTestData(); + $expected = $this->getExpectedNotForcedObjectTestResult(); + + $actual = Json\Json::encode($source, false, ["forceObject" => false]); + + $this->assertEquals($expected, $actual); + } + + /** + * Test Encoder class when clearing the forceObject flag. + */ + public function testNotForcedObjectWithEncoderComponent() + { + Json\Json::$useBuiltinEncoderDecoder = false; + + $source = $this->getForceObjectTestData(); + $expected = $this->getExpectedNotForcedObjectTestResult(); + + $actual = Json\Json::encode($source, false, ["forceObject" => false]); + + $this->assertEquals($expected, $actual); + } + + /** + * Test built in encoder when utilising the default forceObject flag + */ + public function testDefaultForceObjectWithBuiltInEncoder() + { + Json\Json::$useBuiltinEncoderDecoder = true; + + $source = $this->getForceObjectTestData(); + $expected = $this->getExpectedNotForcedObjectTestResult(); + + $actual = Json\Json::encode($source); + + $this->assertEquals($expected, $actual); + } + + /** + * Test Encoder class when utilising the default forceObject flag + */ + public function testDefaultForceObjectWithEncoderComponent() + { + Json\Json::$useBuiltinEncoderDecoder = false; + + $source = $this->getForceObjectTestData(); + $expected = $this->getExpectedNotForcedObjectTestResult(); + + $actual = Json\Json::encode($source); + + $this->assertEquals($expected, $actual); + } } From f40fc6c3d9ccb78d7d17f2fb8147086c394f3a05 Mon Sep 17 00:00:00 2001 From: John Whitley Date: Mon, 23 Sep 2019 08:29:15 +0000 Subject: [PATCH 2/2] Add "forceObject" option to the encoder. This commit adds documentation for the new "forceObject" flag. --- docs/book/advanced.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/book/advanced.md b/docs/book/advanced.md index 552efa95d..aacd6eaab 100644 --- a/docs/book/advanced.md +++ b/docs/book/advanced.md @@ -62,6 +62,22 @@ $jsonObject = Zend\Json\Json::encode( ); ``` +## Forcing arrays to be JSON objects + +If you need the JSON to produce objects in all cases, you may pass the +option `forceObject` in the encode options to force the encoding to +objects only. This often makes the resultant JSON string longer in cases +where the encoding would have automatically chosen an array: however, +the benefit is that this produces a consistent schema. + +```php +$jsonObject = Zend\Json\Json::encode( + $data, + true, + ['forceObject' => true] +); +``` + ## Internal Encoder/Decoder `Zend\Json` has two different modes depending if ext/json is enabled in your PHP