PHP: __toString () и json_encode () не работают вместе
Автор вопроса: Peter BaileyОтветов: 5
У меня возникла странная проблема, и я не знаю, как ее исправить. У меня есть несколько классов, которые представляют собой 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 () никогда не вызывается.
Запоздалые ответы, но могут быть полезны для других с той же проблемой.
В PHP ‹5.4.0json_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 "}]"
Для тех, кто столкнулся с проблемой того, что частные свойства не добавляются, вы можете просто реализовать интерфейс IteratorAggregate с помощью метода getIterator (). Добавьте свойства, которые вы хотите включить в вывод, в массив в методе getIterator () и верните его.
Также включена реализация интерфейса 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
Ответ 4 · pablasso
Даже если ваша защищенная переменная была общедоступной, а не защищенной, у вас не будет желаемого ввода, поскольку в результате будет выведен весь объект следующим образом:
[{"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 );
}
}
В документации есть некоторые ответы на эту проблему (даже если мне не нравится большинство из них, сериализация + удаление свойств заставляет меня чувствовать себя грязным).
Ответ 5 · null
Вы правы, __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() );