We have moved our forum to GitHub Discussions. For questions about Phalcon v3/v4/v5 you can visit here and for Phalcon v6 here.

Decoding JSON to a model class object instead of stdClass

When I decode JSON I end up with an array of stdClasses of course. Does Phalcon have a utility to decode JSON or automatically convert my stdClass to the desired model classes? I can write a utillity to do this--and I did look through the Model docs but I did not see anything about this. It would be awesome to have a native C method performing this task, as it can get very labor intensive.

edited Mar '14

Something along these lines:

https://stackoverflow.com/questions/5397758/json-decode-to-custom-class

I'm decoding this JSON into $input:

{"token":"607f0db8d5306285948ea1ed93a9f69c","data":[{"id":"1","users_id":"2","name":"TestBudget","cash":"1900","bills":[{"id":"1","budgets_id":"1","name":"Car","amount":"290.25"}],"expenses":[{"id":"1","budgets_id":"1","name":"Food","amount":"175"}],"sharedWith":[]}]}

Doing this:

$budget_data = $input->data;

print_r($budget_data);

Shows this:

Array ( [0] => stdClass Object ( [id] => 1 [users_id] => 2 [name] => TestBudget [cash] => 1900 [bills] => Array ( [0] => stdClass Object ( [id] => 1 [budgets_id] => 1 [name] => Car [amount] => 290.25 ) ) [expenses] => Array ( [0] => stdClass Object (
[id] => 1 [budgets_id] => 1 [name] => Food [amount] => 175 ) ) [sharedWith] => Array ( ) ) )

Bills and Expenses could be the hints for the class type and I guess I would need to ensure that the entire mess has the top-level array member keyed as Budget.

edited Mar '14

Wrote this little ditty--works good:

    private function budgetClassObjectDistiller($decoded_json){

        $budget = new Budgets();
        $expenses = array();
        $bills = array();

        $data = $decoded_json[0];

        foreach ($data AS $key => $value) {

            if (is_array($value) && $key === "bills"){

                foreach($value as $b){
                    $bill = new Bills();
                    foreach ($b AS $b_key => $b_value){
                        $bill->{$b_key} = $b_value;
                    }
                    $bills[] = $bill;
                }

            }else if (is_array($value) && $key === "expenses"){

                foreach($value as $e){
                    $expense = new Expenses();
                    foreach ($e AS $e_key => $e_value){
                        $expense->{$e_key} = $e_value;
                    }
                    $expenses[] = $expense;
                }

            }else{      
                $budget->{$key} = $value;
            }
        }

        $budget->expenses = $expenses;
        $budget->bills = $bills;
        return $budget;
    }

At this point I must say: Phalcon rocks! Best framework I have ever tested....so smooth and functional that my testing led straight into full-blown development with no setbacks at all. The ORM is awesome. I just jammed in a new Expense in my input with no populated identifiers and it handled everyting perfectly.

I haven't examined the situation enough to know if it is possible but adding a generic auto class distiller like the hardcoded one modeled above would be off the hook.

A generic function would be possible if you either a) changed your JSON to have some sort of type hinting - I'd recommend a new property used just for this purpose - something like "typeHint", or b) Didn't deal with nested objects

In the first case, you could just write a generic function that set object properties of whatever object was created, then have it return that newly created object.

In the second case, you could write a class method to do the same thing essentially, just from inside the class.

edited Mar '14

As you can see above, I did use the array property as the object hint..and I could write a generic method to do this but I thought it would make more sense to have this baked into Phalcon. This can get quite slow if you have a lot of data or many objects with more properties. In a non-trivial REST API you are going to have to handle a lot of JSON input and eventually get it converted to class-typed objects. After I implented the "distiller" method above it reduced my LOC significantly. All of the lines removed where around data type checking of the passed in JSON objects.

I thought of creating a method in the model class but that would require you to maintain that method for any database changes. This capability screams for integration with the base Phalcon feature set. Doing this in a generic way can be very slow relative to having it implemented in native C.



125.7k
Accepted
answer

Agreed: a PHP implementation would be slower than a C implementation.

As for model based function - you wouldn't need to update it, you could just do something like this:

foreach($json as $key=>$value){
    $this->{$key} = $value;
}
edited Mar '14

Working solution:

        $input = RestUtil::decodeJSON($json);

        $budget_data = new Budgets();

        $budget_data->promote($input->data); 

$json looks like this:

{
id: "1"
users_id: "2"
name: "TestBudget"
cash: "1985.23"
-Bills: [
-{
id: "1"
budgets_id: "1"
name: "Car"
amount: "350.25"
}
]
-Expenses: [
-{
id: "1"
budgets_id: "1"
name: "Food"
amount: "600"
}
-{
id: "2"
budgets_id: "1"
name: "Gas"
amount: "250"
}
]
SharedWith: [ ]
}

$input->data looks like this:

stdClass Object ( 
    [id] => 1 [users_id] => 2 [name] => TestBudget [cash] => 1985.23 
    [Bills] => Array ( 
                        [0] => stdClass Object ( [id] => 1 [budgets_id] => 1 [name] => Car [amount] => 350.25 ) 
                     ) 
    [Expenses] => Array ( 
                        [0] => stdClass Object ( [id] => 1 [budgets_id]=> 1 [name] => Food [amount] => 600 ) 
                        [1] => stdClass Object ( [id] => 2 [budgets_id] => 1 [name] => Gas [amount] => 250 ) 
                        ) 
    [SharedWith] => Array ( ) 
)

Budgets extends EnhancedModel which extends Phalcon\Mvc\Model (see promote()):

<?php
use Phalcon\Mvc\Model;

abstract class EnhancedModel extends Model
{

    public function promote($std_class, $target_class=null){
        try{

            /*
             * << Establish the parent Model type >>
             * Allow the user to specify the target_class by providing a string or object. 
             * Assume extending class when null.
             */ 
            if (is_null($target_class)){    
                $object = $this;
            }elseif (is_object($target_class)){
                $object = $target_class;
            }elseif (is_string($target_class)){
                $object = new $target_class();  //New up a Model from a user-supplied class name--usually during recursion
            }else {
                return $std_class;  //Not playing nice?  Return the data passed in.
            }

            /* 
             * Prepare this array in case we encounter an array of related records nested in our stdClass
             * Each element will be an array of one or more...the children being instances of the related Model (related to the parent).
             */

            $related_entities = array();  //eventual array of arrays

            /* Loop through the stdClass, accessing properties like an array. */
            foreach ($std_class as $property => $value) {

                /* 
                 * If an array is found as the value of a property we assume it is full of realted entities; 
                 * with the property name being the Model type (case sensitive)
                 * 
                 */ 
                if (is_array($value)){  //all of these are stdClass as well, so we recurse to handle each one

                    /* 
                     * $property should be named to fit the model of the entities in the array 
                     * This is dependent on the user building the JSON object correctly upstream.
                     *  
                     */
                    $related_entities[$property] = array(); 

                    foreach($value as $entity){  //Get each array element and treat it as an entity
                        /* 
                         * For thought-simplicity sake, let's assume this promote() call doesn't find related entities inside this related entity (Yo Dawg...). 
                         * This adds the related entity to an array named for its Model: $related_entities['related_model_name'] = $object_returned_from_promote(). 
                         * This WILL, of course, recurse to infinity building out the complete data model.
                         */
                         $related_entities[$property] = $this->promote($entity, $property); 
                    }

                }else {
                        /* Just add the value found to the property of the Model object */  
                        $object->{$property} = $value;
                }           
            }       

            /*  
             * Add each array of whatever related entities were found, to the parent object/table
             * This depends on the Phalcon ORM Model convention: $MyTableObject->relatedSomthings = array_of_related_somethings
             */
            foreach($related_entities as $related_model => $entity_data){
                    $object->{$related_model} = $entity_data;       
            }

        }catch(Exception $e){
            /* 
             * If the user supplied data (decoded JSON) that does not match the Model we are going to experience an exception
             * when trying to access a property that doesn't exist. 
             * 
             */ 
            throw $e;
        }
        return $object; /* Usually only important when we are using recursion. */
    }   
}

I have not considered many-to-many, and I have not tested this with a one-to-one relationship. Working great for one-to-many. Anything stand out?