Hello everybody,
[size="5"]A sobering story (skip if just interested in some results)[/size]
since I’m currently working on a JSON/REST API I needed an easy way of creating more complex JSON responses than the one that CJSON generates. To be more specific, my REST-API needed nested JSON-objects, meaning that it should follow any of my CActiveRecord relation and include their properties as well. Furthermore, I needed only parts of these objects so I wrote a little array-based JSON description syntax. Using this I could “easily” define a JSON response like this:
$jsonSetup = array(
'Exercise', array(
'uid',
'version',
'name_de' => 'name',
'categories:[uid]' => 'category_uid',
'subcategories:[uid]' => 'subcategory_uid',
'steps' => array(
'sort' => 'order',
'text_de' => 'text',
'image:image_url' => 'imageURL'
),
)
);
This configuration says:
-
Given CActiveRecord objects of type ‘Exercise’
-
Embed the properties: uid and version, as well as the property name_de (renamed to name)
-
Follow relation ‘categories’ and embedd the property ‘uid’ of all related objects as simple array (Since I deliver categories seperately, I just send their IDs here)
-
Same with ‘subcategories’, I name the field inside the JSON feed ‘subcategory_uid’
-
Follow relation ‘steps’ and embed them as array of JSON objects with following properties: sort (renamed to ‘order’), text_de (renamed to ‘text’), again follow relation image (second hierarchy) and embedd property image_url
-
etc…
After defining the JSON structure sending was as simple as:
$exercises = Exercise::model()->find($someCriteria);
$restAPI->sendAsJSON($exercises, $jsonSetup);
The servers JSON response looked somethink like this:
[
{
'uid': '...',
'version': '...',
'name': '...',
'category_uid': [1,2,3,4],
'subcategory_uid': [5,6,7,8],
'steps': [
{ 'order': 0, 'test': '...', 'imageURL': 'h t t p://...' },
{ 'order': 1, 'test': '...', 'imageURL': 'h t t p://...' },
{ 'order': 2, 'test': '...', 'imageURL': 'h t t p://...' },
]
},
{ 'uid' ..... }
]
This looked promising in the first place, but soon I recognized that the array-based description syntax was way to limited. I needed more than the existing set of "commands" so I hacked them into the syntax. Soon it was hard to read, harder to write, and quite annoying when it came to debugging.
[size="5"]RESULTS - A story of success[/size]
So I changed the paradigm of how I created the JSON and tried another approach using "plain" (or nearly plain) data arrays. I created a wrapper called JSONArray that allows me to minimize the coding overhead when working with associative arrays (especially writing the brackets for field access). Using my JSONArray class a JSON feed is now generated imperative, other than the previous declarative approach:
$dbExercises = Exercise::model()->find($someCriteria);
$exercises = new JSONArray();
foreach ($dbExercises as $model) {
$exercise = new JSONArray();
$exercise->uid = $model->uid;
$exercise->name = $model->name_de;
$exercise->version = $model->version;
foreach ($model->categories as $modCategory) {
$exercise->category_uid->add($modCategory->uid);
}
foreach ($model->subcategories as $modSubcategory) {
$exercise->subcategory_uid->add($modSubcategory->uid);
}
foreach ($model->steps as $modStep) {
$step = new JSONArray();
$step->sort = (integer) $modStep->sort;
$step->text = $modStep->text_de;
$step->imageURL = $modStep->image->image_url;
$exercise->steps->add($step);
}
$exercises->add($exercise);
}
$restAPI->sendText(CJSON::encode($exercises->toArray()));
The response of the server would be the same. The additional overhead is acceptable, since using this approach ANY structure including any content can be returned the same way (e.g. generating random values inside the feed, would be hard to hack into the first declarative approach).
The JSONArray class itself is fairly simple. Merely it is a array-wrapper using PHPs magic getters/setters. Therefore:
$jsonArray->someKey = 'someValue';
is the object oriented and more easy way of writing
$array['someKey'] = 'someValue';
Furthermore, the class automatically creates further JSONArrays when accessing an empty property. So it is possible to chain hierarchies as easy as
// First way
$jsonArray->level1->level2->key = 'value';
// Same as above
$jsonArray->level1 = new JSONArray();
$jsonArray->level1->level2 = new JSONArray();
$jsonArray->level1->level2->key = 'value';
To prevent this behaviour the JSONArray object can be set to read-only, returning null when a value is not set. This way the JSONArray can also be used for reading JSON POST requests from a client.
$postData = file_get_contents("php://input");
if (!$postData) {
return NULL;
}
// Create the JSONArray from associative array as read only
$jsonArray = new JSONArray(CJSON::decode($postData), TRUE);
$value = $jsonArray->value;
if($value === null) { ... }
I would be glad to hear some comments about my approach. I attached my JSONArray class, maybe someone is interested.
Greetings David