PHP:__toString() и json_encode() плохо сочетаются друг с другом
-
03-07-2019 - |
Вопрос
Я столкнулся со странной проблемой и не уверен, как ее исправить.У меня есть несколько классов, которые все являются 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.0 json_encode
не вызывает никакого метода из объекта.Это справедливо для getIterator, __serialize и т.д...
Однако в PHP версии 5.4.0 был представлен новый интерфейс, называемый JsonSerializable (сериализуемый).
Он в основном управляет поведением объекта, когда json_encode
вызывается для этого объекта.
Скрипка (Ну, на самом деле это 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);
Результаты:
строка(29) "[{"foo":"bar"},{"foo":"bar"}]"
Другие советы
Разве ваш ответ не в PHP docs для json_encode?
Для тех, кто столкнулся с проблемой, когда частные свойства не добавляются, вы можете просто реализовать интерфейс IteratorAggregate с помощью метода getIterator().Добавьте свойства, которые вы хотите включить в выходные данные, в массив в методе getIterator() и верните его.
В PHP > версии 5.4.0 вы можете реализовать интерфейс, называемый JsonSerializable
как описано в ответ Автор: Тиви.
Для тех из нас, кто использует 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 в исходном классе с получателем по умолчанию и вызывать значения напрямую
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 );
}
}
Примечание:(строка), приведенная перед новыми буквами B...это вызовет метод _toString() класса B, но он не даст вам того, что вы хотите, потому что вы столкнетесь с классическими проблемами "двойного кодирования", потому что массив закодирован в методе _toString() класса B, и он будет закодирован снова в методе класса _toString().
Таким образом, есть выбор декодирования результата после приведения, т.е.:
$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() );