3

I am using PHP 5.2.x and want to encode objects of my custom PHP classes with only private members. One of the private members is an array of objects of another custom class.

I tried the solution outlined in https://stackoverflow.com/a/7005915/17716, but that obviously does not work recursively. The only solution that I can see is extending the json_encode method somehow ,so that the class' version of the json_encode method is called instead of the default method.

For reference, the code is as follows:

Class A {
    private $x;
    private $y;
    private $z;
    private $xy;
    private $yz;
    private $zx;

    public function f1() {
        ...
    }

    public function f2() {
        ...
    }
    .
    .
    .
    public function getJSONEncode() {
         return json_encode(get_object_vars($this));
    }
}

class B {

    private $p; //This finally stores objects of class A
    private $q;
    private $r;

    public function __construct() {
            $this->p = array();
    }

    public function fn1() {
        ...
    }

    public function fn2() {
        ...
    }

    .
    .
    .

    public function getJSONEncode() {
         return json_encode(get_object_vars($this));
    }

}

class C {

    private $arr;

    public function __construct() {
            $this->arr = array();
    }

    public function fillData($data) {
        $obj = new B();
        //create objects of class B and fill in values for all variables
        array_push($this->arr, $obj)
    }

    public function returnData() {
          echo $this->arr[0]->getJSONEncode(); //Edited to add

    }

}

What would be the best way to achieve this nested json encoding?

Edited to Add:

The output I get when the returnData method is executed is:

{"p":[{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}],"q":"Lorem Ipsum","r":"Dolor Sit Amet"}
Community
  • 1
  • 1
AJ.
  • 1,146
  • 11
  • 33
  • 1
    I don't see any Nested json here .. `Class A { class PostAttributes {` is invalid .. Am not sure i totally get what you want – Baba Oct 08 '12 at 17:14
  • Apologies and thanks for catching that. The "Class PostAttributes {" was a copy paste error. Also added the comment that the array P contains objects of Class A – AJ. Oct 08 '12 at 17:21
  • what exactly are you executing .. its not in the above code – Baba Oct 08 '12 at 17:23
  • Your code formatting is a little off there. – TRiG Oct 08 '12 at 17:24
  • @Baba: Not sure I understand your question... Can you elaborate? – AJ. Oct 08 '12 at 17:33
  • The nature of private values is to be private, why would you want them to then translate to a data construct that has no concept of private? Are you expecting to send them back to JavaScript? -- if so, why not use public items instead, considering they will be public eventually? – Pebbl Oct 08 '12 at 17:34
  • @AJ How did get your output ?? what .... – Baba Oct 08 '12 at 17:34
  • @pebbl Yes I return it to a frontend for processing by a JS library – AJ. Oct 08 '12 at 17:36
  • @Baba: Class C->returnData(); //I added an echo statement there for simplicity's purposes. Just assume that returnData is called somewhere – AJ. Oct 08 '12 at 17:36
  • Ok, So what happens if you stop using private elements and set them to public instead? – Pebbl Oct 08 '12 at 17:37
  • The objects will be encoded. However, I prefer not to make public the properties of the classes merely for encoding. I have additional code which will access data including to and from a DB and prefer to use proper getter and setters for clean access/writes to the data – AJ. Oct 08 '12 at 17:42

1 Answers1

2

Whilst I believe you would be better off writing a proper export/encode function for each of your classes (which would construct a public object from both private and public values just for encoding - you could be quite clever and use php's reflection ability) - instead - you could make use of the following code:

/// example class B
class TestB {
  private $value = 123;
}

/// example class A
class TestA {
  private $prop;
  public function __construct(){
    $this->prop = new TestB();
  }
}

function json_encode_private($obj){
  /// export the variable to find the privates
  $exp = var_export($obj, true);
  /// get rid of the __set_state that only works 5.1+
  $exp = preg_replace('/[a-z0-9_]+\:\:__set_state\(/i','((object)', $exp);
  /// rebuild the object
  eval('$enc = json_encode('.$exp.');');
  /// return the encoded value
  return $enc;
}

echo json_encode_private(new TestA());

/// {"prop":{"value":123}}

So the above should work, but I wouldn't recommend using eval anywhere in php - just because I always hear alarm bells quietly off in the distance :)

update

Just had a thought of what might make this a little safer, rather than using eval you could use create_function which would limit some of its creational powers, or at least the scope of those powers...

function json_encode_private($obj){
  $exp = var_export($obj, true);
  $exp = preg_replace('/[a-z0-9_]+\:\:__set_state\(/i','((object)', $exp);
  $enc = create_function('','return json_encode('.$exp.');');
  return $enc();
}

update 2

Had a chance to play around with another way of converting an object with private properties, to an object with public properties - using only a simple function (and no eval). The following would need to be tested on whichever version of PHP you are using as it's behaviour - again - might not be reliable.... due to the weird \0Class Name\0 prefixing in the converted private properties (see comments in code).

For more info on this strange prefixing behaviour:
http://uk3.php.net/language.types.array.php#language.types.array.casting

Anyway, so using a test class:

class RandomClass {
  private $var = 123;
  private $obj;
  public function __construct(){
    $this->obj = (object) null;
    $this->obj->time = time();
  }
}

We can use the following function to convert it to a public object:

function private_to_public( $a ){
  /// grab our class, convert our object to array, build our return obj
  $c = get_class( $a ); $b = (array) $a; $d = (object) null;
  /// step each property in the array and move over to the object
  /// usually this would be as simple as casting to an object, however
  /// my version of php (5.3) seems to do strange things to private 
  /// properties when casting to an array... hence the code below:
  foreach( $b as $k => $v ){
    /// for some reason private methods are prefixed with a \0 character
    /// and then the classname, followed by \0 before the actual key value. 
    /// This must be some kind of internal protection causing private  
    /// properties to be ignored. \0 is used by some languges to terminate  
    /// strings (not php though, as far as i'm aware).
    if ( ord($k{0}) === 0 ) {
      /// trim off the prefixed weirdnesss..?!
      $e = substr($k, 1 + strlen($c) + 1);
      /// unset the $k var first because it will remember the \0 even 
      /// if other values are assigned to it later on....?!
      unset($k); $k = $e;
    }
    /// so if we have a key, either public or private - set our value on
    /// the destination object.
    if ( $k !== '' && $k !== NULL && $k !== FALSE )  {
      $d->{$k} = $v;
    }
  }
  return $d;
}

So if we put it all together:

$a = new RandomClass();

echo json_encode( private_to_public( $a ) );

/// {"var":123,"obj":{"time":1349777323}}

Again your best / most reliable bet is to either bespokely code your conversion methods for each class, or create some kind of generalised solution using Class Reflection, but that latter is far more involved, more involved than a StackOverflow answer... at least with the amount of time I have free ;)

further info

The above code will work when trying to access objects from anywhere, the reason for implementing this was because my first attempt was obviously to use the following:

echo json_encode( get_object_vars($a) );

/// you will get {} which isn't what you expect

It seems that if you want to use get_object_vars you have to use it from a context that has access to all the properties, i.e. from inside the class you are exposing:

public function getAllProperties(){
  return get_object_vars( $this );
}

So, imagine we'd added the above to the RandomClass definition:

echo json_encode( $a->getAllProperties() );

/// you will get {"var":123,"obj":{"time":1349777323}}

This works because the members of a class have access to all the class's properties public or private.... so as I say, working this way is far far superior; superior, but not always possible.

Pebbl
  • 34,937
  • 6
  • 62
  • 64
  • Sorry, I'm not entirely familiar with out OO works in PHP, but isn't there anyway that I can extend the json_encode method (or my classes) so that when encoding is tried, they automatically get called? - Something similar to what JSONSerialize makes possible but in PHP 5.4 and above – AJ. Oct 09 '12 at 04:50
  • No, not that I know of, it's thre reason why [JSONSerialize](http://php.net/manual/en/jsonserializable.jsonserialize.php) was created I guess. There are the magic functions `__sleep` and `__wakeup` but they only work for `serialize` and `unserialize`. And there is the magic function `__set_state` but that is for use with `var_export`. http://php.net/manual/en/language.oop5.magic.php - just found this other SO question along the same lines too http://stackoverflow.com/questions/8778020/how-to-control-json-encode-behavior *(no non-hack answers there either)* – Pebbl Oct 09 '12 at 08:13
  • Thank you for the variety of options to workaround this. I'm also going to try and push my hosting provider to upgrade their version of PHP so that I can use the simpler JSONSerialize interface. Until them, you're answer is the way to go :) – AJ. Oct 09 '12 at 13:32
  • @AJ. No problem... your question forced me to learn something new about `get_object_vars` so all is good! Yep asking them to upgrade sounds like a plan :) only possible problem to that is php 5.4 was released 2012-03-01 and most hosts that I know of would only ever consider upgrading after a release had been in the wild for a least a year - if not more. If you can get a dedicated server you can do what you like though, although that would work out as rather an expensive json_encode function ;) – Pebbl Oct 09 '12 at 13:57