PHP:__toString() и json_encode() плохо сочетаются друг с другом

StackOverflow https://stackoverflow.com/questions/401908

  •  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() );
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top