PHP: __toString () и json_encode () не работают вместе

У меня возникла странная проблема, и я не знаю, как ее исправить. У меня есть несколько классов, которые представляют собой PHP-реализации объектов JSON. Вот иллюстрация проблемы

class A
{
    protected $a;

    public function __construct()
    {
        $this->a = array( new B, new B );
    }

    public function __toString()
    {
        return json_encode( $this->a );
    }
}

class B
{
    protected $b = array( 'foo' => 'bar' );

    public function __toString()
    {
        return json_encode( $this->b );
    }
}

$a = new A();

echo $a;

Результатом этого будет

[{},{}]

Когда желаемый результат

[{"foo":"bar"},{"foo":"bar"}]

Проблема в том, что я полагался на ловушку __toString (), чтобы делать мою работу за меня. Но это невозможно, потому что сериализация, которую использует json_encode (), не будет вызывать __toString (). Когда он встречает вложенный объект, он просто сериализует только общедоступные свойства.

Итак, тогда возникает вопрос: есть ли способ разработать управляемый интерфейс для классов JSON, который позволяет мне использовать сеттеры и геттеры для свойств, но также позволяет мне получить желаемое поведение сериализации JSON?

Если это не ясно, вот пример реализации, которая не работает, поскольку ловушка __set () вызывается только для начального присваивания.

class a
{
    public function __set( $prop, $value )
    {
        echo __METHOD__, PHP_EOL;
        $this->$prop = $value;
    }

    public function __toString()
    {
        return json_encode( $this );
    }
}

$a = new a;
$a->foo = 'bar';
$a->foo = 'baz';

echo $a;

Полагаю, я мог бы сделать что-то вроде этого

class a
{
    public $foo;

    public function setFoo( $value )
    {
        $this->foo = $value;
    }

    public function __toString()
    {
        return json_encode( $this );
    }
}

$a = new a;
$a->setFoo( 'bar' );

echo $a;

Но тогда мне пришлось бы полагаться на старание других разработчиков, чтобы использовать сеттеры - я не могу программно принудить к соблюдению этого решения.

---> РЕДАКТИРОВАТЬ ‹---

Теперь с тестом ответа Роба Элснера

<?php

class a implements IteratorAggregate 
{
    public $foo = 'bar';
    protected $bar = 'baz';

    public function getIterator()
    {
        echo __METHOD__;
    }
}

echo json_encode( new a );

Когда вы выполните это, вы увидите, что метод getIterator () никогда не вызывается.


person Peter Bailey    schedule 31.12.2008    source источник


Ответы (5)


Запоздалые ответы, но могут быть полезны для других с той же проблемой.

В PHP ‹5.4.0 json_encode не вызывает никаких методов из объекта. Это действительно для getIterator, __serialize и т. Д.

Однако в PHP> v5.4.0 был представлен новый интерфейс, названный JsonSerializable .

Он в основном управляет поведением объекта, когда для этого объекта вызывается json_encode.


Пример:

class A implements JsonSerializable
{
    protected $a = array();

    public function __construct()
    {
        $this->a = array( new B, new B );
    }

    public function jsonSerialize()
    {
        return $this->a;
    }
}

class B implements JsonSerializable
{
    protected $b = array( 'foo' => 'bar' );

    public function jsonSerialize()
    {
        return $this->b;
    }
}


$foo = new A();

$json = json_encode($foo);

var_dump($json);

Выходы:

строка (29) "[{" foo ":" bar "}, {" foo ":" bar "}]"

person Tivie    schedule 24.04.2013

Разве ваш ответ не содержится в документации PHP для json_encode?

Для тех, кто столкнулся с проблемой того, что частные свойства не добавляются, вы можете просто реализовать интерфейс IteratorAggregate с помощью метода getIterator (). Добавьте свойства, которые вы хотите включить в вывод, в массив в методе getIterator () и верните его.

person Rob Elsner    schedule 31.12.2008
comment
Но это не работает, или я делаю неправильно. Я отредактировал свой вопрос, чтобы включить этот пример. - person Peter Bailey; 31.12.2008
comment
В качестве альтернативы, можете ли вы создать общедоступные переменные в качестве этих свойств и переопределить __get для этой общедоступной переменной, чтобы вернуть частное значение, и __set для этого имени переменной, чтобы вызвать исключение? - person Rob Elsner; 01.01.2009
comment
Да, глядя на источник PHP и некоторые другие комментарии, я бы порекомендовал вам объявить общедоступные свойства, отключить их в конструкторе, а затем переопределить __get, чтобы вернуть личные данные для этого имени переменной. - person Rob Elsner; 01.01.2009
comment
Это тоже не сработает, потому что тогда json_encode не найдет переменные, поскольку они больше не являются общедоступными. - person Peter Bailey; 05.01.2009

В PHP ›v5.4.0 вы можете реализовать интерфейс под названием JsonSerializable, как описано в ответ от Тиви .

Для тех из нас, кто использует PHP ‹5.4.0, вы можете использовать решение, в котором используется get_object_vars() из самого объекта, а затем передает их в json_encode() < / а>. Это то, что я сделал в следующем примере, используя метод __toString(), так что, когда я преобразовываю объект в виде строки, я получаю представление в кодировке JSON.

Также включена реализация интерфейса IteratorAggregate с его _ 6_, чтобы мы могли перебирать свойства объекта, как если бы они были массивом.

<?php
class TestObject implements IteratorAggregate {
    
  public $public = "foo";
  protected $protected = "bar";
  private $private = 1;
  private $privateList = array("foo", "bar", "baz" => TRUE);
  
  /**
   * Retrieve the object as a JSON serialized string
   *
   * @return string
   */
  public function __toString() {
    $properties = $this->getAllProperties();

    $json = json_encode(
      $properties,
      JSON_FORCE_OBJECT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
    );

    return $json;
  }

  /**
   * Retrieve an external iterator
   *
   * @link http://php.net/manual/en/iteratoraggregate.getiterator.php
   * @return \Traversable
   *  An instance of an object implementing \Traversable
   */
  public function getIterator() {
    $properties = $this->getAllProperties();
    $iterator = new \ArrayIterator($properties);

    return $iterator;
  }

  /**
   * Get all the properties of the object
   *
   * @return array
   */
  private function getAllProperties() {
    $all_properties = get_object_vars($this);

    $properties = array();
    while (list ($full_name, $value) = each($all_properties)) {
      $full_name_components = explode("\0", $full_name);
      $property_name = array_pop($full_name_components);
      if ($property_name && isset($value)) $properties[$property_name] = $value;
    }

    return $properties;
  }

}

$o = new TestObject();

print "JSON STRING". PHP_EOL;
print "------" . PHP_EOL;
print strval($o) . PHP_EOL;
print PHP_EOL;

print "ITERATE PROPERTIES" . PHP_EOL;
print "-------" . PHP_EOL;
foreach ($o as $key => $val) print "$key -> $val" . PHP_EOL;
print PHP_EOL;

?>

Этот код дает следующий результат:

JSON STRING
------
{"public":"foo","protected":"bar","private":1,"privateList":{"0":"foo","1":"bar","baz":true}}

ITERATE PROPERTIES
-------
public -> foo
protected -> bar
private -> 1
privateList -> Array
person Christopher    schedule 24.10.2013

Даже если ваша защищенная переменная была общедоступной, а не защищенной, у вас не будет желаемого ввода, поскольку в результате будет выведен весь объект следующим образом:

[{"b":{"foo":"bar"}},{"b":{"foo":"bar"}}]

Вместо:

[{"foo":"bar"},{"foo":"bar"}]

Скорее всего, это приведет к поражению вашей цели, но я более склонен конвертировать в json в исходном классе с помощью геттера по умолчанию и напрямую вызывать значения

class B
{
    protected $b = array( 'foo' => 'bar' );

    public function __get($name)
    {
        return json_encode( $this->$name );
    }
}

Затем вы можете делать с ними все, что захотите, даже вкладывая значения в дополнительный массив, как это делает ваш класс A, но используя json_decode ... он все еще кажется несколько грязным, но работает.

class A
{
    protected $a;

    public function __construct()
    {
        $b1 = new B;
        $b2 = new B;
        $this->a = array( json_decode($b1->b), json_decode($b2->b) );
    }

    public function __toString()
    {
        return json_encode( $this->a );
    }
}

В документации есть некоторые ответы на эту проблему (даже если мне не нравится большинство из них, сериализация + удаление свойств заставляет меня чувствовать себя грязным).

person pablasso    schedule 31.12.2008

Вы правы, __toString () для класса B не вызывается, потому что для этого нет причин. Чтобы назвать это, вы можете использовать гипс

class A
{
    protected $a;

    public function __construct()
    {
        $this->a = array( (string)new B, (string)new B );
    }

    public function __toString()
    {
        return json_encode( $this->a );
    }
}

Примечание: приведение (string) перед новым B ... это вызовет метод _toString () класса B, но он не даст вам того, что вы хотите, потому что вы столкнетесь с классическими проблемами "двойного кодирования" , поскольку массив закодирован в методе _toString () класса B, и он будет закодирован снова в методе _toString () класса A.

Таким образом, есть выбор декодирования результата после приведения, то есть:

 $this->a = array( json_decode((string)new B), json_decode((string)new B) );

или вам нужно будет получить массив, создав метод toArray () в классе B, который возвращает прямой массив. Это добавит код в строку выше, потому что вы не можете напрямую использовать конструктор PHP (вы не можете использовать новый B () -> toArray ();) Таким образом, у вас может быть что-то вроде:

$b1 = new B;
$b2 = new B;
$this->a = array( $b1->toArray(), $b2->toArray() );
person null    schedule 31.12.2008