views:

1024

answers:

6

Suppose I have a User class with 'name' and 'password' properties, and a 'save' method. When serializing an object of this class to JSON via json_encode, the method is properly skipped and I end up with something like {'name': 'testName', 'password': 'testPassword'}.

However, when deserializing via json_decode, I end up with a StdClass object instead of a User object, which makes sense but this means the object lacks the 'save' method. Is there any way to cast the resultant object as a User, or to provide some hint to json_decode as to what type of object I'm expecting?

+4  A: 

I think the best way to handle this would be via the constructor, either directly or via a factory:

class User
{
   public $username;
   public $nestedObj; //another class that has a constructor for handling json
   ...

   // This could be make private if the factories below are used exclusively
   // and then make more sane constructors like:
   //     __construct($username, $password)
   public function __construct($mixed)
   {
       if (is_object($mixed)) {
           if (isset($mixed->username))
               $this->username = $mixed->username;
           if (isset($mixed->nestedObj) && is_object($mixed->nestedObj))
               $this->nestedObj = new NestedObject($mixed->nestedObj);
           ...
       } else if (is_array($mixed)) {
           if (isset($mixed['username']))
               $this->username = $mixed['username'];
           if (isset($mixed['nestedObj']) && is_array($mixed['nestedObj']))
               $this->nestedObj = new NestedObj($mixed['nestedObj']);
           ...
       }
   }
   ...

   public static fromJSON_by_obj($json)
   {
       return new self(json_decode($json));
   }

   public static fromJSON_by_ary($json)
   {
       return new self(json_decode($json, TRUE)); 
   }
}
Ryan Graham
Exactly. JSON has no way of encoding what kind of object the original was.
R. Bemrose
+2  A: 

Short answer: No (not that I know of*)

Long answer: json_encode will only serialize public variables. As you can see per the JSON spec, there is no "function" datatype. These are both reasons why your methods aren't serialized into your JSON object.

Ryan Graham is right - the only way to re-create these objects as non-stdClass instances is to re-create them post-deserialization.

Example

<?php

class Person
{
    public $firstName;
    public $lastName;

    public function __construct( $firstName, $lastName )
    {
     $this->firstName = $firstName;
     $this->lastName = $lastName;
    }

    public static function createFromJson( $jsonString )
    {
     $object = json_decode( $jsonString );
     return new self( $object->firstName, $object->lastName );
    }

    public function getName()
    {
     return $this->firstName . ' ' . $this->lastName;
    }
}

$p = new Person( 'Peter', 'Bailey' );
$jsonPerson = json_encode( $p );

$reconstructedPerson = Person::createFromJson( $jsonPerson );

echo $reconstructedPerson->getName();

Alternatively, unless you really need the data as JSON, you can just use normal serialization and leverage the __sleep() and __wakeup() hooks to achieve additional customization.

* In a previous question of my own it was suggested that you could implement some of the SPL interfaces to customize the input/output of json_encode() but my tests revealed those to be wild goose chases.

Peter Bailey
+1  A: 

I'm aware that JSON doesn't support the serialization of functions, which is perfectly acceptable, and even desired. My classes are currently used as value objects in communicating with JavaScript, and functions would hold no meaning (and the regular serialization functions aren't usable).

However, as the functionality pertaining to these classes increases, encapsulating their utility functions (such as a User's save() in this case) inside the actual class makes sense to me. This does mean they're no longer strictly value objects though, and that's where I run into my aforementioned problem.

An idea I had would have the class name specified inside the JSON string, and would probably end up like this:

$string = '{"name": "testUser", "password": "testPassword", "class": "User"}';
$object = json_decode ($string);
$user = ($user->class) $object;

And the reverse would be setting the class property during/after json_encode. I know, a bit convoluted, but I'm just trying to keep related code together. I'll probably end up taking my utility functions out of the classes again; the modified constructor approach seems a bit opaque and runs into trouble with nested objects.

I do appreciate this and any future feedback, however.

Jeroen van Delft
Take a look at the J2EE world where you would create a separate class for encapsulating the operations you would be performing on the data-only object. In this case, maybe User and UserHandler.
Ryan Graham
I've updated my answer to address nested objects.
Ryan Graham
+1  A: 

You could create a FactoryClass of some sort:

function create(array $data)  
{
    $user = new User();
    foreach($data as $k => $v) {
        $user->$k = $v;
    }
    return $user;
}

It's not like the solution you wanted, but it gets your job done.

bouke
A: 

To answer your direct question, no, there's no was to do this with json_encode/json_decode. JSON was designed and specified to be a format for encoding information, and not for serializing objects. The PHP function don't go beyond that.

If you're interested in recreating objects from JSON, one possible solution is a static method on all the objects in your hierarchy that accepts a stdClass/string and populates variables that looks something like this

//semi pseudo code, not tested
static public function createFromJson($json){
 //if you pass in a string, decode it to an object
 $json = is_string($json) ? json_decode($json) : $json;

 foreach($json as $key=>$value){
  $object = new self();
  if(is_object($value)){
   $object->{$key} = parent::createFromJson($json);
  }
  else{
   $object->{$key} = $value;
  }
 }

 return $object;
}

I didn't test that, but I hope it gets the idea across. Ideally, all your objects should extend from some base object (usually named "class Object") so you can add this code in one place only.

Alan Storm
A: 

I must say I am a bit dismayed that this isn't just standard, off the shelf functionality -- of some library, if not JSON itself. Why wouldn't you want to have essentially similar objects on both sides? (As far as JSON will let you, anyway)

Am I missing something here? Is there a library that does this? (AFAICT, none of [thrift, protocol buffers, avro] actually have API's for javascript. For my problem, I am most interested in JS <-> PHP, somewhat also in JS <-> python .)

Nick Papadakis