【问题标题】:PHP: __toString() and json_encode() not playing well togetherPHP:__toString() 和 json_encode() 不能很好地配合使用
【发布时间】:2010-09-28 22:48:59
【问题描述】:

我遇到了一个奇怪的问题,我不知道如何解决它。我有几个类都是 JSON 对象的 PHP 实现。这是问题的说明

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 类的托管接口,既可以让我使用 setter 和 getter 来获取属性,又可以让我获得我想要的 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;

但是我将不得不依靠其他开发人员的勤奋来使用设置器 - 我无法通过此解决方案以编程方式强制遵守。

---> 编辑

现在测试一下 Rob Elsner 的反应

<?php

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

    public function getIterator()
    {
        echo __METHOD__;
    }
}

echo json_encode( new a );

执行此操作时,您可以看到从未调用过 getIterator() 方法。

【问题讨论】:

    标签: php json serialization


    【解决方案1】:

    一个迟到的答案,但可能对其他有同样问题的人有用。

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

    输出:

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

    【讨论】:

      【解决方案2】:

      PHP docs for json_encode不是你的答案吗?

      对于遇到未添加私有属性问题的任何人,您可以简单地使用 getIterator() 方法实现 IteratorAggregate 接口。将要包含在输出中的属性添加到 getIterator() 方法中的数组中并返回。

      【讨论】:

      • 但这不起作用,或者我做错了。我编辑了我的问题以包含此示例。
      • 或者,您可以创建公共变量作为这些属性,并覆盖该公共变量上的 __get 以返回私有值,并对该变量名进行 __set 以引发异常吗?
      • 是的,查看 PHP 源代码和其他一些 cmets,我建议您声明公共属性,在构造函数中取消设置它们,然后覆盖 __get 以返回该变量名的私有数据。跨度>
      • 这也行不通,因为 json_encode 将找不到变量,因为它们不再是公开的。
      【解决方案3】:

      在 PHP > v5.4.0 中,您可以实现名为JsonSerializable 的接口,如the answer 中的Tivie 所述。

      对于我们这些使用 PHP 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
      

      【讨论】:

        【解决方案4】:

        即使您的受保护变量是公开的而不是受保护的,您也不会获得所需的输入,因为这将像这样输出整个对象:

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

        代替:

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

        这很可能会破坏您的目的,但我更倾向于使用默认 getter 转换为原始类中的 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 );
            }
        }
        

        documentation 中有一些对这个问题的回应(即使我不喜欢他们中的大多数,序列化+剥离属性让我觉得很脏)。

        【讨论】:

          【解决方案5】:

          你是对的,B 类的 __toString() 没有被调用,因为没有理由这样做。所以要调用它,你可以使用演员表

          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 之前的 (string) 强制转换 ...这将调用 B 类的 _toString() 方法,但它不会得到你想要的,因为你会遇到经典的“double encoding”问题,因为数组是在B类的_toString()方法中编码的,在A类的_toString()方法中会再次编码。

          所以有一个选择是在强制转换后对结果进行解码,即:

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

          或者你需要通过在 B 类中创建一个返回直接数组的 toArray() 方法来获取数组。这将在上面的行中添加一些代码,因为你不能直接使用 PHP 构造函数(你不能做一个 new B()->toArray(); )所以你可以有类似的东西:

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2014-05-15
            • 2018-07-31
            • 2016-12-26
            • 1970-01-01
            • 1970-01-01
            • 2013-04-16
            相关资源
            最近更新 更多