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, который позволяет мне использовать сеттеры и getters для свойств, но также позволяет мне получить поведение сериализации 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; 

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

—> EDIT <—

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

 <?php class a implements IteratorAggregate { public $foo = 'bar'; protected $bar = 'baz'; public function getIterator() { echo __METHOD__; } } echo json_encode( new a ); 

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

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

В PHP <5.4.0 json_encode не вызывает никакого метода из объекта. Это верно для getIterator, __serialize и т. Д. …

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

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


Fiddle (Ну, на самом деле это PHP CodePad, но то же самое …)

Пример :

 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); 

Выходы:

string (29) "[{" foo ":" bar "}, {" foo ":" bar "}]"

Не ваш ответ в документах PHP для json_encode ?

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

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

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

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

 <?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 

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

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

Вместо:

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

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

 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 ); } } 

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

Вы правы __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, но он не даст вам то, что вы хотите, потому что вы столкнетесь с классическими проблемами «двойного кодирования» , потому что массив закодирован в методе класса B _toString (), и он будет снова закодирован в методе класса A _String ().

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

  $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() );