魔術方法是 PHP 物件模型的「元編程入口」。本文從引擎視角深度解析 __sleep/__wakeup 與新式 __serialize/__unserialize 協議差異、__debugInfo 內部實現、__call/__callStatic 動態代理模式,並對比 PHP 8.1+ readonly、newInitializers 等現代特性對魔術方法的替代效果。
__sleep/__wakeup vs __serialize/__unserializeclass LegacyUser {
private $pdo; // PDO 不可序列化
private $data = [];
// 舊協議 (PHP 5.1+)
public function __sleep(): array {
return ['data']; // 僅返回需序列化的屬性名
}
public function __wakeup(): void {
$this->pdo = new PDO('sqlite::memory:'); // 重建連接
$this->data['last_login'] = time(); // 修復狀態
}
}
// 問題:__sleep 返回屬性名數組,無法控制鍵值結構;__wakeup 易受反序列化攻擊
class ModernUser {
private $pdo;
private $data = [];
// 新協議 (PHP 7.4+ 推薦)
public function __serialize(): array {
// 完全控制序列化輸出結構
return [
'data' => $this->data,
'version' => 2,
// 故意排除 $pdo
];
}
public function __unserialize(array $data): void {
// 完全控制反序列化邏輯,參數為關聯數組
$this->data = $data['data'] ?? [];
// 版本遷移邏輯
if (($data['version'] ?? 1) < 2) {
$this->migrateV1toV2();
}
$this->pdo = null; // 延遲重建
}
private function migrateV1toV2(): void {
// 數據結構升級
}
}
核心差異:__serialize 返回完整鍵值結構 (而非屬性名列表),__unserialize 接收關聯數組 (而非依賴屬性名匹配),支持版本遷移、結構重組、安全過濾。
__debugInfo 內部機制與性能class HeavyObject {
private $hugeData = []; // 100MB 數據
private $secret = 'api_key_123';
public function __debugInfo(): array {
// var_dump 時只輸出摘要,避免內存爆炸/敏感洩漏
return [
'data_summary' => [
'count' => count($this->hugeData),
'keys' => array_slice(array_keys($this->hugeData), 0, 10),
],
'secret' => '***HIDDEN***',
];
}
}
// 內部實現 (簡化):
// function var_dump($var) {
// if (is_object($var) && method_exists($var, '__debugInfo')) {
// $debug = $var->__debugInfo();
// // 輸出 $debug 而非對象所有屬性
// }
// }
// 性能影響: 大對象圖 var_dump 會遞歸調用 __debugInfo,慎用複雜邏輯
__call/__callStatic 實現門面class ServiceFacade {
private $services = [];
public function register(string $name, object $service): void {
$this->services[$name] = $service;
}
// 實例方法代理: $facade->user()->getName()
public function __call(string $name, array $args) {
if (!isset($this->services[$name])) {
throw new \BadMethodCallException("Service [$name] not registered");
}
$service = $this->services[$name];
// 支援鏈式調用: 返回服務本身
return $service;
}
// 靜態方法代理: ServiceFacade::user()->getName()
public static function __callStatic(string $name, array $args) {
$instance = static::getInstance();
return $instance->$name(...$args);
}
}
// 使用
$facade = new ServiceFacade();
$facade->register('user', new UserService());
$facade->register('order', new OrderService());
$facade->user()->create(['name' => 'John']); // 代理到 UserService
$facade->order()->find(123); // 代理到 OrderService
| 魔術方法 | 現代替代 (PHP 8.1+) | 優勢 |
|---|---|---|
__get/__set 動態屬性 | readonly 屬性 + 構造函數提升 | 靜態分析、IDE 支援、不可變性、性能 |
__call 方法代理 | 顯式介面 + 依賴注入 | 類型安全、可重構、編譯時檢查 |
__sleep/__wakeup | __serialize/__unserialize | 結構化、版本化、安全 |
__clone 深拷貝 | readonly + with* 方法 (Value Object 模式) | 不可變、無副作用、線程安全 |
__invoke 可調用對象 | 閉包 / fn() 箭頭函數 | 輕量、語法糖、自動捕獲作用域 |
__set_state | 棄用,改用工廠方法 ::fromArray() | 無 eval 風險、類型安全 |
__get/__set/__clone)// PHP 8.1+
readonly class Money {
public function __construct(
public int $cents,
public string $currency = 'CNY'
) {}
// 無 __get/__set,屬性不可變
// 無 __clone,clone 會報錯 (readonly 類不可 clone)
// 不可變操作返回新實例
public function add(Money $other): self {
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException("Currency mismatch");
}
return new self($this->cents + $other->cents, $this->currency);
}
public function multiply(float $factor): self {
return new self((int)round($this->cents * $factor), $this->currency);
}
public function toString(): string {
return bcdiv((string)$this->cents, '100', 2) . ' ' . $this->currency;
}
}
$price = new Money(1999, 'CNY');
$total = $price->add(new Money(500))->multiply(1.06);
// $price 未被修改,線程安全、無副作用
allowed_classes 防禦// 用戶可控輸入反序列化 (極高風險)
$data = $_COOKIE['session'] ?? '';
$user = @unserialize($data, ['allowed_classes' => [User::class, Session::class]]);
// 只有白名單類會被實例化並觸發 __wakeup/__unserialize
// 其他類會被轉換為 __PHP_Incomplete_Class 對象
需要動態行為?
├─ 是 → 是否為框架基礎設施/庫代碼?
│ ├─ 是 → 謹慎使用魔術方法,完善文檔、測試、靜態分析規則
│ └─ 否 → 重構為顯式介面/依賴注入/值對象
└─ 否 → 完全避免魔術方法,使用現代 PHP 特性
特定場景必用魔術方法:
✅ __construct/__destruct: 資源管理 (RAII)
✅ __serialize/__unserialize: 序列化控制
✅ __debugInfo: 調試輸出控制
✅ __call/__callStatic: 門面/代理/動態路由 (框架核心)
⚠️ __get/__set/__isset/__unset: 僅限 ORM/ActiveRecord 基類
❌ __set_state: 棄用
❌ __invoke: 改用閉包
❌ __clone: 改用 readonly 類 + with* 方法
魔術方法是雙刃劍:在框架核心、基礎設施層賦予極大表達力;在業務代碼中導致隱式行為、難以重構、靜態分析失效。現代 PHP (8.1+) 通過 readonly、構造函數提升、__serialize 等特性,將 80% 魔術方法場景轉化為類型安全、可重構的顯式代碼。