調用第三方 API、ORM 實體、XML 解析結果常返回 `stdClass` 物件。直接 `json_decode(json_encode($obj), true)` 雖快但有缺陷:丟失 `private/protected` 屬性、無法處理循環引用、資源類型報錯。本文提供健壯的遞歸轉換函數,支援可見性過濾、循環引用檢測、資源類型跳過。
<?php
/**
* 將任意物件/陣列深度轉為純陣列
*
* @param mixed $data 物件、陣列、標量
* @param bool $includePrivate 是否包含 private/protected 屬性 (需 Reflection)
* @param array &$seen 內部用:已訪問物件 spl_object_id 防循環
* @return array
*/
function objectToArray($data, bool $includePrivate = false, array &$seen = []): array {
if (is_object($data)) {
// 循環引用檢測
$oid = spl_object_id($data);
if (isset($seen[$oid])) {
return ['*CIRCULAR_REF*' => get_class($data)];
}
$seen[$oid] = true;
// 取得屬性
if ($includePrivate) {
$refl = new ReflectionObject($data);
$props = [];
foreach ($refl->getProperties() as $prop) {
$prop->setAccessible(true);
$props[$prop->getName()] = $prop->getValue($data);
}
} else {
$props = get_object_vars($data); // 僅 public
}
$result = [];
foreach ($props as $key => $val) {
$result[$key] = objectToArray($val, $includePrivate, $seen);
}
unset($seen[$oid]);
return $result;
}
if (is_array($data)) {
$result = [];
foreach ($data as $key => $val) {
$result[$key] = objectToArray($val, $includePrivate, $seen);
}
return $result;
}
// 資源類型 (stream, curl, mysqli, pdo...) 無法序列化,標記跳過
if (is_resource($data)) {
return ['*RESOURCE*' => get_resource_type($data)];
}
// 標量直接返回
return $data;
}
// 便捷別名
function std_class_object_to_array($obj) {
return objectToArray($obj, false);
}
?>
<?php
class User {
public $id = 1;
public $name = 'Alice';
protected $email = 'alice@example.com';
private $passwordHash = 'bcrypt$...';
public $profile = null; // 稍後賦值形成循環引用
}
$user = new User();
$user->profile = (object)['bio' => 'Hi', 'user' => $user]; // 循環引用
// 1. 僅 public (默認)
print_r(objectToArray($user));
/* 輸出:
Array
(
[id] => 1
[name] => Alice
[profile] => Array
(
[bio] => Hi
[user] => Array
(
[*CIRCULAR_REF*] => User
)
)
)
*/
// 2. 包含 private/protected (需 PHP 8.1+ ReflectionProperty::getValue 無需 setAccessible)
print_r(objectToArray($user, true));
/* 輸出包含 email、passwordHash */
?>
| 特性 | json_encode/decode | 遞歸函數 objectToArray |
|---|---|---|
| 速度 | 極快 (C 實現) | 中等 (PHP 迴圈) |
| private/protected | 丟失 | 可選保留 (Reflection) |
| 循環引用 | 報錯 `Recursion detected` | 檢測並標記 `*CIRCULAR_REF*` |
| 資源類型 | 報錯/轉 null | 標記 `*RESOURCE*` 類型名 |
| DateTime 等物件 | 轉 ISO8601 字串 | 保持物件 (可擴展轉換) |
| 記憶體 | 雙倍 (中間 JSON 字串) | 單次遍歷 |
<?php
function objectToArrayAdvanced($data, array $converters = [], array &$seen = []): array {
if (is_object($data)) {
$oid = spl_object_id($data);
if (isset($seen[$oid])) return ['*CIRCULAR_REF*' => get_class($data)];
$seen[$oid] = true;
// 自定義轉換器優先 (如 DateTime => timestamp)
$class = get_class($data);
if (isset($converters[$class])) {
$result = $converters[$class]($data);
unset($seen[$oid]);
return $result;
}
$props = get_object_vars($data);
$result = [];
foreach ($props as $k => $v) {
$result[$k] = objectToArrayAdvanced($v, $converters, $seen);
}
unset($seen[$oid]);
return $result;
}
// ... 陣列/標量同前
}
// 使用
$converters = [
DateTime::class => fn($d) => $d->getTimestamp(),
DateTimeImmutable::class => fn($d) => $d->getTimestamp(),
// 自定義 Entity => toArray()
];
$arr = objectToArrayAdvanced($entity, $converters);
?>
symfony/serializer、jms/serializer 成熟庫。