tags:

views:

234

answers:

5

I am trying to json_encode an array of objects who all have magic properties using __get and __set. json_encode completely ignores these, resulting in an array of empty objects (all the normal properties are private or protected).

So, imagine this class:

class Foo
{
    public function __get($sProperty)
    {
        if ($sProperty == 'foo')
        {
            return 'bar!';
        }
        return null;
    }
}

$object = new Foo();
echo $object->foo; // echoes "foo"
echo $object->bar; // warning
echo json_encode($object); // "{}"

I've tried implementing IteratorAggregate and Serializable for the class, but json_encode still doesn't see my magic properties. Since I am trying to encode an array of these objects, an AsJSON()-method on the class won't work either.

Update! It seems the question is easy to misunderstand. How can I tell json_encode which "magic properties" exist? IteratorAggregate didn't work.

BTW: The term from the PHP documentation is "dynamic entities". Whether or not the magic properties actually exist is arguing about semantics.

A: 

This is the correct behavior
JSON is only able to contain data not methods - it is meant to be language independent so encoding object methods would not make sense.

meouw
I am asking about the magic **properties**, not the methods.
Vegard Larsen
There are no magic **properties**, just magic **methods** that get called when you try to access non-declared **properties**.
Chacha102
+2  A: 

From your comment:

I am asking about the magic properties, not the methods. – Vegard Larsen 1 min ago

 

There is no such thing as a magic property.

 

All you have right now is a magic method that gets called when you try to access a non-visible property.

Let's say it again

 

$obj->foo does not exist

 

The way a magic method is implemented is not a concern of PHP, and it can't magically know that you are using your magic method to 'magically' make it look like there is $obj->foo.

If a property does not exist, it will not be put into the object when you json_encode it.

Furthermore, even if json_encode knew that __get was active, it wouldn't know what value to use to call it.

Chacha102
So, the fact that the property "doesn't exist" in the object is of no interest to me. Please see the revised question.
Vegard Larsen
Well, if the property doesn't exist, it isn't going to be put into JSON.
Chacha102
And there is NO way to alert `json_encode` to this? Implementing `IteratorAggregate` alerts `foreach` of the properties; why doesn't `json_encode` pick this up?
Vegard Larsen
PHP didn't get the name `broken language` for nothing....
Chacha102
A: 

How could it know your properties?

$object = new Foo();
echo $object->foo; // how do you know to access foo and not oof?
                   // how do you expect json_encode to know to access foo?

magic methods are syntactic sugar, and mostly fire back when you use them. this is one such case.

echo json_encode($object); // "{}"

of course it's empty, $object has zero public properties, just a magic method and the ensuing syntactic sugar (turned sour)

just somebody
A: 

I'd create a method on the object to return the internal array.

class Foo
{
    private $prop = array();

    public function __get($sProperty)
    {
       return $this->prop[$sProperty];
    }

    public function __set($sProperty, $value)
    {
       $this->prop[$sProperty] = $value;
    }

    public function getJson(){
        return json_encode($this->prop);
    }
}



$f = new Foo();
$f->foo = 'bar';
$json = $f->getJson();
Simon
What happens when you try to `json_encode` an array of these objects? `json_encode` won't know to call `getJson()`. This was mentioned in the question, as well.
Vegard Larsen
The idea is not not use `json_encode` .... it is to call the getJSON on the object instead.
Chacha102
Look at the exmaple after the class, as Chacha102 says, you use the getJson method I've added to the class and this then does know which properties exist because it can access the class internals.
Simon
I have an array of these objects. Calling `json_encode` on every single object in the array is not exactly a clean way of solving this.
Vegard Larsen
Well, with the structure you have, it seems to be the only way to do it.you may have been better off not using the magic __get functions and just having a stdClass objects that you attach public properies to
Simon
The magic `__get` is required for other reasons.
Vegard Larsen
+1  A: 

json_encode() doesn't "asks" the object for any interface. It directly fetches the HashTable pointer that represents the properties of an object by calling obj->get_properties(). It then iterates (again directly, no interface such as Traversable, Iterator etc. is used) over this HashTable and processes the elements that are marked as public. see static void json_encode_array() in ext/json/json.c
That makes it impossible to have a property to show up in the result of json_encode() but not to be accessible as $obj->propname.

edit: I haven't tested it much and forget about "high performance" but you might want to start with

interface EncoderData {
  public function getData();
}

function json_encode_ex_as_array(array $v) {
  for($i=0; $i<count($v); $i++) {
    if ( !isset($v[$i]) ) {
      return false;
    }
  }
  return true;
}

define('JSON_ENCODE_EX_SCALAR', 0);
define('JSON_ENCODE_EX_ARRAY', 1);
define('JSON_ENCODE_EX_OBJECT', 2);
define('JSON_ENCODE_EX_EncoderDataObject', 3);

function json_encode_ex($v) {
  if ( is_object($v) ) {
    $type = is_a($v, 'EncoderData') ? JSON_ENCODE_EX_EncoderDataObject : JSON_ENCODE_EX_OBJECT;
  }
  else if ( is_array($v) ) {
    $type = json_encode_ex_as_array($v) ? JSON_ENCODE_EX_ARRAY : JSON_ENCODE_EX_OBJECT;
  }
  else {
    $type = JSON_ENCODE_EX_SCALAR;
  }

  switch($type) {
    case JSON_ENCODE_EX_ARRAY: // array [...]
      foreach($v as $value) {
        $rv[] = json_encode_ex($value);
      }
      $rv = '[' . join(',', $rv) . ']';
      break;
    case JSON_ENCODE_EX_OBJECT: // object { .... }
      $rv = array();
      foreach($v as $key=>$value) {
        $rv[] = json_encode((string)$key) . ':' . json_encode_ex($value);
      }
      $rv = '{' . join(',', $rv) .'}';
      break;
    case JSON_ENCODE_EX_EncoderDataObject:
      $rv = json_encode_ex($v->getData());
      break;
    default:
      $rv = json_encode($v);
  }
  return $rv;
}

class Foo implements EncoderData {
  protected $name;
  protected $child;

  public function __construct($name, $child) {
    $this->name = $name;
    $this->child = $child;

  }
  public function getData() {
    return array('foo'=>'bar!', 'name'=>$this->name, 'child'=>$this->child);
  }
}


$data = array();
for($i=0; $i<10; $i++) {
  $root = null;
  foreach( range('a','d') as $name ) {
    $root = new Foo($name, $root);
  }
  $data[] = 'iteration '.$i;
  $data[] = $root;
  $root = new StdClass;
  $root->i = $i;
  $data[] = $root;
}
$json = json_encode_ex($data);
echo $json, "\n\n\n";
$data = json_decode($json);
var_dump($data);

There is at least one flaw: It doesn't handle recursion, e.g.

$obj = new StdClass;
$obj->x = new StdClass;
$obj->x->y = $obj;
echo json_encode($obj); // warning: recursion detected...
echo json_encode_ex($obj); // this one runs until it hits the memory limit
VolkerK