From a204c136fe2dc0a3971b5b5fedcfec00da83db67 Mon Sep 17 00:00:00 2001 From: Julian Seidenberg Date: Wed, 23 Mar 2011 10:23:57 +1300 Subject: [PATCH] API CHANGE: JSONDataFormatter builds JSON itself, changing it to use Convert::array2json() instead (fixes #5162, thanks sharvey) --- api/JSONDataFormatter.php | 82 ++++++++++++++++++--------------- core/ArrayData.php | 12 +++++ tests/ArrayDataTest.php | 9 ++++ tests/api/RestfulServerTest.php | 16 +++++++ 4 files changed, 83 insertions(+), 36 deletions(-) diff --git a/api/JSONDataFormatter.php b/api/JSONDataFormatter.php index 2ce7476ca..eeebc1bc3 100644 --- a/api/JSONDataFormatter.php +++ b/api/JSONDataFormatter.php @@ -26,27 +26,38 @@ class JSONDataFormatter extends DataFormatter { } /** - * Generate an XML representation of the given {@link DataObject}. - * - * @param DataObject $obj - * @param $includeHeader Include header (Default: true) - * @return String XML + * Generate a JSON representation of the given {@link DataObject}. + * + * @param DataObject $obj The object + * @param Array $fields If supplied, only fields in the list will be returned + * @param $relations Not used + * @return String JSON */ public function convertDataObject(DataObjectInterface $obj, $fields = null, $relations = null) { + return Convert::array2json($this->convertDataObjectToJSONObject($obj, $fields, $relations)); + } + + /** + * Internal function to do the conversion of a single data object. It builds an empty object and dynamically + * adds the properties it needs to it. If it's done as a nested array, json_encode or equivalent won't use + * JSON object notation { ... }. + * @param DataObjectInterface $obj + * @param $fields + * @param $relations + * @return EmptyJSONObject + */ + public function convertDataObjectToJSONObject(DataObjectInterface $obj, $fields = null, $relations = null) { $className = $obj->class; $id = $obj->ID; - - $json = "{\n \"className\" : \"$className\",\n"; + + $serobj = ArrayData::array_to_object(); + foreach($this->getFieldsForObj($obj) as $fieldName => $fieldType) { // Field filtering if($fields && !in_array($fieldName, $fields)) continue; $fieldValue = $obj->$fieldName; - if(is_object($fieldValue) && is_subclass_of($fieldValue, 'Object') && $fieldValue->hasMethod('toJSON')) { - $jsonParts[] = "\"$fieldName\" : " . $fieldValue->toJSON(); - } else { - $jsonParts[] = "\"$fieldName\" : " . Convert::raw2json($fieldValue); - } + $serobj->$fieldName = $fieldValue; } if($this->relationDepth > 0) { @@ -63,24 +74,24 @@ class JSONDataFormatter extends DataFormatter { } else { $href = Director::absoluteURL(self::$api_base . "$className/$id/$relName"); } - $jsonParts[] = "\"$relName\" : { \"className\" : \"$relClass\", \"href\" : \"$href.json\", \"id\" : \"{$obj->$fieldName}\" }"; + $serobj->$relName = ArrayData::array_to_object(array("className" => $relClass, "href" => "$href.json", "id" => $obj->$fieldName)); } - + foreach($obj->has_many() as $relName => $relClass) { if(!singleton($relClass)->stat('api_access')) continue; - + // Field filtering if($fields && !in_array($relName, $fields)) continue; if($this->customRelations && !in_array($relName, $this->customRelations)) continue; - $jsonInnerParts = array(); + $innerParts = array(); $items = $obj->$relName(); foreach($items as $item) { //$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID"); $href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID"); - $jsonInnerParts[] = "{ \"className\" : \"$relClass\", \"href\" : \"$href.json\", \"id\" : \"{$obj->$fieldName}\" }"; + $innerParts[] = ArrayData::array_to_object(array("className" => $relClass, "href" => "$href.json", "id" => $obj->$fieldName)); } - $jsonParts[] = "\"$relName\" : [\n " . implode(",\n ", $jsonInnerParts) . " \n ]"; + $serobj->$relName = $innerParts; } foreach($obj->many_many() as $relName => $relClass) { @@ -90,42 +101,41 @@ class JSONDataFormatter extends DataFormatter { if($fields && !in_array($relName, $fields)) continue; if($this->customRelations && !in_array($relName, $this->customRelations)) continue; - $jsonInnerParts = array(); + $innerParts = array(); $items = $obj->$relName(); foreach($items as $item) { //$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID"); $href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID"); - $jsonInnerParts[] = " { \"className\" : \"$relClass\", \"href\" : \"$href.json\", \"id\" : \"{$obj->$fieldName}\" }"; + $innerParts[] = ArrayData::array_to_object(array("className" => $relClass, "href" => "$href.json", "id" => $obj->$fieldName)); } - $jsonParts[] = "\"$relName\" : [\n " . implode(",\n ", $jsonInnerParts) . "\n ]"; + $serobj->$relName = $innerParts; } } - return "{\n " . implode(",\n ", $jsonParts) . "\n}"; } + return $serobj; + } /** - * Generate an XML representation of the given {@link DataObjectSet}. + * Generate a JSON representation of the given {@link DataObjectSet}. * * @param DataObjectSet $set * @return String XML */ public function convertDataObjectSet(DataObjectSet $set, $fields = null) { - $jsonParts = array(); - foreach($set as $item) { - if($item->canView()) $jsonParts[] = $this->convertDataObject($item, $fields); - } - $json = "{\n"; - $json .= '"totalSize": '; - $json .= (is_numeric($this->totalSize)) ? $this->totalSize : 'null'; - $json .= ",\n"; - $json .= "\"items\": [\n" . implode(",\n", $jsonParts) . "\n]\n"; - $json .= "}\n"; + $items = array(); + foreach ($set as $do) $items[] = $this->convertDataObjectToJSONObject($do, $fields); - return $json; + $serobj = ArrayData::array_to_object(array( + "totalSize" => (is_numeric($this->totalSize)) ? $this->totalSize : null, + "items" => $items + )); + + return Convert::array2json($serobj); } public function convertStringToArray($strData) { return Convert::json2array($strData); } - -} \ No newline at end of file + +} +?> \ No newline at end of file diff --git a/core/ArrayData.php b/core/ArrayData.php index 484c15b56..a9a2f4871 100755 --- a/core/ArrayData.php +++ b/core/ArrayData.php @@ -97,6 +97,18 @@ class ArrayData extends ViewableData { return $arr; } + + /** + * Converts an associative array to a simple object + * + * @param array + * @return obj $obj + */ + public static function array_to_object($arr = null) { + $obj = new stdClass(); + if ($arr) foreach($arr as $name => $value) $obj->$name = $value; + return $obj; + } /** * This is pretty crude, but it helps diagnose error situations diff --git a/tests/ArrayDataTest.php b/tests/ArrayDataTest.php index 373465190..6f5c931ea 100644 --- a/tests/ArrayDataTest.php +++ b/tests/ArrayDataTest.php @@ -97,6 +97,15 @@ class ArrayDataTest extends SapphireTest { $this->assertEquals($arrayData->getArray(), $array); } + function testArrayToObject() { + $arr = array("test1" => "result1","test2"=>"result2"); + $obj = ArrayData::array_to_object($arr); + $objExpected = new stdClass(); + $objExpected->test1 = "result1"; + $objExpected->test2 = "result2"; + $this->assertEquals($obj,$objExpected, "Two objects match"); + } + } class ArrayDataTest_ArrayData_Exposed extends ArrayData { diff --git a/tests/api/RestfulServerTest.php b/tests/api/RestfulServerTest.php index fb9f5e4b5..927875508 100644 --- a/tests/api/RestfulServerTest.php +++ b/tests/api/RestfulServerTest.php @@ -357,6 +357,22 @@ class RestfulServerTest extends SapphireTest { $this->assertEquals($responseArr['Rating'], 42); $this->assertNotEquals($responseArr['WriteProtectedField'], 'haxx0red'); } + + public function testJSONDataFormatter() { + $formatter = new JSONDataFormatter(); + $single_do = $this->objFromFixture('Member', 'editor'); + + $this->assertEquals( + $formatter->convertDataObject($single_do, array("FirstName", "Email")), + '{"FirstName":"Editor","Email":"editor@test.com"}', + "Correct JSON formatting with field subset"); + + $set = DataObject::get("Member"); + $this->assertEquals( + $formatter->convertDataObjectSet($set, array("FirstName", "Email")), + '{"totalSize":null,"items":[{"FirstName":"Editor","Email":"editor@test.com"},{"FirstName":"User","Email":"user@test.com"},{"FirstName":"ADMIN","Email":"ADMIN@example.org"}]}', + "Correct JSON formatting on a dataobjectset with field filter"); + } public function testApiAccessWithPOST() { $url = "/api/v1/RestfulServerTest_AuthorRating";