PHP 中对象和数组的界限并不像其他语言那样严格。stdClass 对象、ORM 返回的模型实例、配置读取器产出的节点对象,经常需要在数组和对象之间切换。转换方式看似简单,但如果只使用类型强转,往往只处理了第一层;如果对象里还嵌套对象或包含私有属性,结果可能和预期相差很大。本文围绕几种实际可用的转换方案展开,对比它们的适用边界和隐藏代价。

一、最简单直接的 json_decode 组合
在 PHP 里把对象转成数组,流传最广的一行代码就是 json_decode(json_encode($object), true)。它的原理非常直白:先通过 json_encode 把对象序列化成 JSON 字符串,再利用 json_decode 的第二个参数 true,让解码结果以关联数组的形式返回,而不是默认的 stdClass 对象。整个过程不依赖对象的具体类定义,也不用手动遍历属性。
这种方式的真正优势在于多层嵌套结构可以一次性处理。比如一个对象属性下面还有对象,对象内部又包含数组,只要这些数据都满足 JSON 序列化条件,转换结果会自动递归成多维数组。下面是一个典型示例:
<?php
class Order
{
public $id = 101;
public $customer;
public $items = [];
public function __construct()
{
$this->customer = new stdClass();
$this->customer->name = 'Tom';
$this->customer->city = 'Shanghai';
$this->items[] = ['sku' => 'A001', 'qty' => 2];
}
}
$order = new Order();
$array = json_decode(json_encode($order), true);
print_r($array);
执行后得到的 $array 完全是一个嵌套关联数组,可以直接用于配置合并、数据库写入或 API 响应。不过这个简便方法也有明显限制。首先 json_encode 只能序列化对象的公开属性,protected 和 private 属性默认会被忽略。其次,如果对象中包含资源类型、闭包、匿名函数或循环引用,JSON 编码会直接失败并返回 false。此外,浮点数在 JSON 编码过程中可能出现精度变化,特殊字符也可能被转义。对于普通数据传输对象来说这些问题不大,但涉及敏感数据时就需要额外处理。
二、get_object_vars 与类型强转
如果不想经过 JSON 这一层,PHP 还提供了更底层的转换方式。get_object_vars($object) 可以返回对象中可见属性的关联数组。注意这里的「可见」取决于调用位置:在类外部只能拿到 public 属性,而在类内部调用时,protected 和 private 属性同样可以被读取出来。类型强转 (array)$object 的行为略有不同,它会把对象的所有属性都塞进数组,但私有属性的键名会带上特殊前缀,包含类名和空字节,不适合直接使用。
通过下面的代码可以直观看到两种方式的差异:
<?php
class User
{
public $name = 'Alice';
protected $email = 'alice@ippipp.com';
private $age = 30;
}
$user = new User();
// 类外调用只返回 public 属性
var_dump(get_object_vars($user));
// 类型强转会带上 private,但键名含特殊前缀
var_dump((array)$user);
从输出结果可以看到,get_object_vars 在类外部返回的数组只包含 name,而 (array)$user 虽然包含了三个属性,但 email 和 age 的键名已经被污染,形如 Useremail 且夹带空字节。即使把这些非法键名清理掉,另一个问题依然存在:两者都只转换对象本身这一层。如果属性值本身是对象,转换结果里它仍然保持对象类型,只是最外层变成了数组。因此这两种方法更适合结构简单、属性公开的临时对象,不适合深层次数据结构。
三、编写可递归的 object_to_array 函数
要应对嵌套对象,可以自己实现递归转换函数。基本思路是:遇到对象先把它转换成数组,然后遍历数组的每一项,如果值仍然是对象或数组,就递归调用同一个函数继续处理。使用 get_object_vars 作为第一层转换手段时,函数只能拿到公开属性,但这对于大多数数据传输对象已经足够,代码也非常简洁。
<?php
function object_to_array($data)
{
if (is_object($data)) {
$data = get_object_vars($data);
}
if (is_array($data)) {
return array_map(__FUNCTION__, $data);
}
return $data;
}
$object = new stdClass();
$object->name = 'Demo';
$object->detail = new stdClass();
$object->detail->level = 3;
$array = object_to_array($object);
print_r($array);
这个函数会对数组中的每一项执行 __FUNCTION__ 回调,形成递归。由于 PHP 中 __FUNCTION__ 在函数内部会被解析成当前函数名的字符串,因此 array_map 可以正确调用自身。不过它同样受 get_object_vars 的可见性限制。如果必须读取 protected 和 private 属性,就需要借助反射机制。反射可以绕过访问控制,把所有属性强制设为可访问状态,再逐个读取值并递归转换。
<?php
function object_to_array_deep($obj)
{
if (is_object($obj)) {
$reflection = new ReflectionObject($obj);
$properties = $reflection->getProperties();
$result = [];
foreach ($properties as $property) {
$property->setAccessible(true);
$value = $property->getValue($obj);
$result[$property->getName()] = object_to_array_deep($value);
}
return $result;
}
if (is_array($obj)) {
return array_map(__FUNCTION__, $obj);
}
return $obj;
}
class User
{
public $name = 'Alice';
protected $email = 'alice@ippipp.com';
private $age = 30;
}
$user = new User();
$array = object_to_array_deep($user);
print_r($array);
反射方案的优点是完整、通用,可以拿到全部属性,递归深度也不受 JSON 限制。代价是性能较普通遍历差一些,并且如果属性是静态属性或存在循环引用,还需要额外判断,否则可能陷入无限递归。实际使用时可以给递归函数增加深度参数,超过一定层级就停止转换,避免意外情况。
四、实践中的选择与常见坑点
选择哪种方案,取决于对象从哪里来以及你要用数组做什么。如果对象来源于 json_decode 且只包含公开属性,直接使用 json_decode(json_encode($object), true) 是最省事的,代码简短,多层结构自动转换。如果是自己编写的模型类,更推荐在类内部提供一个 toArray() 方法,手动返回需要暴露的字段。这样既能明确控制输出结构,也能避免反射带来的性能损耗。
只有当你面对一个完全未知的第三方对象,又必须拿到所有私有属性时,才考虑反射递归方案。此时还要注意几个常见问题:第一,JSON 编码默认不会把中文转成 Unicode 转义,但如果你对输出格式有要求,可以传入 JSON_UNESCAPED_UNICODE 保持中文可读;第二,ArrayObject 类型的对象不能直接通过 (array) 转换成普通数组,应当调用 getArrayCopy();第三,空对象经过 json_decode(json_encode($obj), true) 会变成空数组,这是正常行为,但可能影响一些严格类型判断。
对于循环引用对象,任何递归转换都有风险。可以通过维护一个已访问对象标识列表来检测重复对象,或在递归函数中设置最大深度。总的来说,PHP 对象转数组没有唯一定式,简便方案优先,复杂场景再逐步升级。
PHP对象转数组json_decodeget_object_vars修改时间:2026-08-29 02:53:59