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/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 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); + } }