将PHP对象序列化为JSON

105

我在php.net上查找有关将PHP对象序列化为JSON的信息时,偶然发现了新的JsonSerializable接口。但这需要 PHP >= 5.4,而我使用的是 5.3.x 环境。

那么,在 PHP < 5.4 的情况下如何实现此功能呢?

我之前没有太多使用 JSON 的经验,但我正在尝试为应用程序支持一个 API 层,将数据对象(本来会被发送到视图的)转换成 JSON 就非常完美了。

如果我直接尝试序列化对象,则返回一个空的 JSON 字符串;这是因为我认为 json_encode()不知道该如何处理该对象。我应该递归地将对象减少为数组,然后编码 该数组 吗?


示例

$data = new Mf_Data();
$data->foo->bar['hello'] = 'world';

echo json_encode($data)会生成一个空对象:

{}

var_dump($data)的输出结果符合预期:

object(Mf_Data)#1 (5) {
  ["_values":"Mf_Data":private]=>
  array(0) {
  }
  ["_children":"Mf_Data":private]=>
  array(1) {
    [0]=>
    array(1) {
      ["foo"]=>
      object(Mf_Data)#2 (5) {
        ["_values":"Mf_Data":private]=>
        array(0) {
        }
        ["_children":"Mf_Data":private]=>
        array(1) {
          [0]=>
          array(1) {
            ["bar"]=>
            object(Mf_Data)#3 (5) {
              ["_values":"Mf_Data":private]=>
              array(1) {
                [0]=>
                array(1) {
                  ["hello"]=>
                  string(5) "world"
                }
              }
              ["_children":"Mf_Data":private]=>
              array(0) {
              }
              ["_parent":"Mf_Data":private]=>
              *RECURSION*
              ["_key":"Mf_Data":private]=>
              string(3) "bar"
              ["_index":"Mf_Data":private]=>
              int(0)
            }
          }
        }
        ["_parent":"Mf_Data":private]=>
        *RECURSION*
        ["_key":"Mf_Data":private]=>
        string(3) "foo"
        ["_index":"Mf_Data":private]=>
        int(0)
      }
    }
  }
  ["_parent":"Mf_Data":private]=>
  NULL
  ["_key":"Mf_Data":private]=>
  NULL
  ["_index":"Mf_Data":private]=>
  int(0)
}

补充

1)

这是我为Mf_Data类设计的toArray()函数:

public function toArray()
{
    $array = (array) $this;
    array_walk_recursive($array, function (&$property) {
        if ($property instanceof Mf_Data) {
            $property = $property->toArray();
        }
    });
    return $array;
}

然而由于 Mf_Data 对象也引用了它们的父对象 (包含对象),这样做会导致递归失败。但是当我移除了 _parent 引用时,它就能完美运行了。

2)

仅作跟进,我最终用于转换复杂树形节点对象的函数是:

// class name - Mf_Data
// exlcuded properties - $_parent, $_index
public function toArray()
{
    $array = get_object_vars($this);
    unset($array['_parent'], $array['_index']);
    array_walk_recursive($array, function (&$property) {
        if (is_object($property) && method_exists($property, 'toArray')) {
            $property = $property->toArray();
        }
    });
    return $array;
}

3)

我再次跟进,提供一个更清晰的实现。使用接口进行instanceof检查似乎比使用method_exists()更加简洁(尽管method_exists()可以跨越继承/实现)。

使用unset()似乎有些凌乱,而且这个逻辑应该重构到另一个方法中。然而,这个实现确实复制了属性数组(由于array_diff_key),因此需要考虑一些事情。

interface ToMapInterface
{

    function toMap();

    function getToMapProperties();

}

class Node implements ToMapInterface
{

    private $index;
    private $parent;
    private $values = array();

    public function toMap()
    {
        $array = $this->getToMapProperties();
        array_walk_recursive($array, function (&$value) {
            if ($value instanceof ToMapInterface) {
                $value = $value->toMap();
            }
        });
        return $array;
    }

    public function getToMapProperties()
    {
        return array_diff_key(get_object_vars($this), array_flip(array(
            'index', 'parent'
        )));
    }

}

4
+1 很好的问题,我之前不知道这个功能。 - takeshin
@takeshin - Yeop,文档页面的编辑日期是4天前。我很高兴看到它! - Dan Lugg
2
对于其他人的参考,json_encode 可以很好地处理对象。但是,它只编码该对象的公共成员。因此,如果您有受保护或私有类变量,则需要使用其中一种发布的方法或 JsonSerializable。 - Matthew Herbst
@MatthewHerbst 当然可以。旧问题已经过时了,而且<5.4现在也不是一个真正的选项(或者至少不应该是)。绝对JsonSerializable - Dan Lugg
11个回答

95
在最简单的情况下,类型提示应该可以正常工作:
$json = json_encode( (array)$object );

7
如果使用命名空间和自动加载器,这会导致属性名冗长且难看。 - BetaRide
4
有没有一种方法可以让属性名称更加清晰简洁? - Christoffer
6
为什么在属性名前面添加\u0000*\u0000? - Elia Weiss
1
私有属性无用。你们都应该学习一下 https://en.wikipedia.org/wiki/Open/closed_principle。 - fabpico

47

编辑:当前是2016年9月24日,PHP 5.4已于2012年3月1日发布,并于2015年9月1日结束支持。然而,这个回答似乎获得了一些赞同。如果您仍在使用PHP < 5.4,您正在创建安全风险并危及您的项目。如果您没有强烈的理由保持在<5.4版本,或者已经使用了>= 5.4版本,请不要使用这个回答,直接使用PHP >= 5.4(或者,您知道的,一个较新的版本)并实现{{link1:JsonSerializable接口}}。
你可以定义一个函数,比如命名为getJsonData();,它可以返回一个数组、stdClass对象或其他具有可见参数而不是私有/受保护参数的对象,并执行json_encode($data->getJsonData());。实质上,实现5.4版本的函数,但手动调用它。
像这样的代码可以工作,因为get_object_vars()是从类内部调用的,可以访问私有/受保护变量:
function getJsonData(){
    $var = get_object_vars($this);
    foreach ($var as &$value) {
        if (is_object($value) && method_exists($value,'getJsonData')) {
            $value = $value->getJsonData();
        }
    }
    return $var;
}

2
谢谢@Wrikken - 有没有快捷方式将一个对象及其包含的对象(所有成员,无论可见性或类型)缩减为关联数组,或将其强制转换为stdClass?我在考虑反射方向,但如果不行,我会想出一些递归执行它的方法。 - Dan Lugg
反射会是一种冗长的方式。由于您在getJsonData()函数中处于类内部,因此可以直接调用get_object_vars(),并循环遍历该结果以查找更多对象。 - Wrikken
我差不多解决了;现在的问题是递归。每个对象都有一个 _parent 属性,所以可以遍历树到根部。请参见我的编辑,也许我应该提出另一个问题,因为这个问题已经从我的原始问题中抽象出来了。 - Dan Lugg
在遍历数组之前,简单地使用unset($array['_parent']);即可解决问题。 - Wrikken
太棒了,谢谢@Wrikken - 我开始尝试复杂的相等性测试,将上下文对象$parent作为用户数据传递给array_walk_recursive()。简单就是美!此外,它是$array["\0class\0property"],因为我使用了强制转换导致了空字节污染。我想我会转而使用get_object_vars() - Dan Lugg

21

json_encode() 仅编码公共成员变量,如果你想包括私有的成员变量,你需要自己来处理(如其他人所建议的)。


12

以下代码使用反射完成任务。它假定您有用于序列化所需属性的getter方法。

    <?php

    /**
     * Serialize a simple PHP object into json
     * Should be used for POPO that has getter methods for the relevant properties to serialize
     * A property can be simple or by itself another POPO object
     *
     * Class CleanJsonSerializer
     */
    class CleanJsonSerializer {

    /**
     * Local cache of a property getters per class - optimize reflection code if the same object appears several times
     * @var array
     */
    private $classPropertyGetters = array();

    /**
     * @param mixed $object
     * @return string|false
     */
    public function serialize($object)
    {
        return json_encode($this->serializeInternal($object));
    }

    /**
     * @param $object
     * @return array
     */
    private function serializeInternal($object)
    {
        if (is_array($object)) {
            $result = $this->serializeArray($object);
        } elseif (is_object($object)) {
            $result = $this->serializeObject($object);
        } else {
            $result = $object;
        }
        return $result;
    }

    /**
     * @param $object
     * @return \ReflectionClass
     */
    private function getClassPropertyGetters($object)
    {
        $className = get_class($object);
        if (!isset($this->classPropertyGetters[$className])) {
            $reflector = new \ReflectionClass($className);
            $properties = $reflector->getProperties();
            $getters = array();
            foreach ($properties as $property)
            {
                $name = $property->getName();
                $getter = "get" . ucfirst($name);
                try {
                    $reflector->getMethod($getter);
                    $getters[$name] = $getter;
                } catch (\Exception $e) {
                    // if no getter for a specific property - ignore it
                }
            }
            $this->classPropertyGetters[$className] = $getters;
        }
        return $this->classPropertyGetters[$className];
    }

    /**
     * @param $object
     * @return array
     */
    private function serializeObject($object) {
        $properties = $this->getClassPropertyGetters($object);
        $data = array();
        foreach ($properties as $name => $property)
        {
            $data[$name] = $this->serializeInternal($object->$property());
        }
        return $data;
    }

    /**
     * @param $array
     * @return array
     */
    private function serializeArray($array)
    {
        $result = array();
        foreach ($array as $key => $value) {
            $result[$key] = $this->serializeInternal($value);
        }
        return $result;
    }  
} 

这是一个很棒的类!它还可以处理受保护的对象项。 - Roelof Berkepeis

7

2

既然你的对象类型是自定义的,我倾向于同意你的解决方案 - 使用编码方法(如JSON或序列化内容)将其分解为较小的段,并在另一端具有相应的代码以重新构建对象。


2

My version:

json_encode(self::toArray($ob))

实现:

private static function toArray($object) {
    $reflectionClass = new \ReflectionClass($object);

    $properties = $reflectionClass->getProperties();

    $array = [];
    foreach ($properties as $property) {
        $property->setAccessible(true);
        $value = $property->getValue($object);
        if (is_object($value)) {
            $array[$property->getName()] = self::toArray($value);
        } else {
            $array[$property->getName()] = $value;
        }
    }
    return $array;
}

JsonUtils : GitHub


正是我所需要的。解决了私有问题。简单而小巧。 - fabpico

1
尝试使用这个,对我来说效果很好。
json_encode(unserialize(serialize($array)));

1

将您的变量类型从private改为public

这样更加简单易读。

例如:

不起作用;

class A{
   private $var1="valuevar1";
   private $var2="valuevar2";
   public function tojson(){
    return json_encode($this)
   }
}

它正在工作;

class A{
   public $var1="valuevar1";
   public $var2="valuevar2";
   public function tojson(){
    return json_encode($this)
   }
}

这很奇怪,但是它是真的。 - Abilogos

0
我创建了一个很好的辅助类,可以将具有get方法的对象转换为数组。它不依赖于属性,只依赖于方法。
所以我有一个包含两个方法的评论对象:
Review
- getAmountReviews:int - getReviews:评论数组
Comment
- getSubject - getDescription
我编写的脚本将把它转换为一个看起来像这样的属性数组:
    {
      amount_reviews: 21,
      reviews: [
        {
          subject: "In een woord top 1!",
          description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque laoreet lacus quis eros venenatis, sed tincidunt mi rhoncus. Aliquam ut pharetra diam, nec lobortis dolor."
        },
        {
          subject: "En een zwembad 2!",
          description: "Maecenas et aliquet mi, a interdum mauris. Donec in egestas sem. Sed feugiat commodo maximus. Pellentesque porta consectetur commodo. Duis at finibus urna."
        },
        {
          subject: "In een woord top 3!",
          description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque laoreet lacus quis eros venenatis, sed tincidunt mi rhoncus. Aliquam ut pharetra diam, nec lobortis dolor."
        },
        {
          subject: "En een zwembad 4!",
          description: "Maecenas et aliquet mi, a interdum mauris. Donec in egestas sem. Sed feugiat commodo maximus. Pellentesque porta consectetur commodo. Duis at finibus urna."
       },
       {
          subject: "In een woord top 5!",
          description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque laoreet lacus quis eros venenatis, sed tincidunt mi rhoncus. Aliquam ut pharetra diam, nec lobortis dolor."
    }
]}

源代码: PHP序列化器,将对象转换为可编码为JSON的数组。

您只需要在输出周围包装json_encode即可。

有关脚本的一些信息:

  • 仅添加以get开头的方法
  • 忽略私有方法
  • 忽略构造函数
  • 方法名称中的大写字符将被替换为下划线和小写字符

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接