魔術方法 是 PHP 面向對象的「鉤子機制」,以雙下劃線 __ 開頭,在特定生命周期或操作時自動觸發。掌握它們能實現屬性/方法重載、序列化控制、對象克隆定制、調試輸出格式化等高級功能。本文系統梳理 12 個魔術方法、__debugInfo (5.6+) 新增特性,並給出生產級實戰模式。
| 方法 | 觸發時機 | 典型用途 | 返回值要求 |
|---|---|---|---|
__construct(...$args) | 對象實例化 new Class() | 依賴注入、屬性初始化、資源獲取 | 無 (void) |
__destruct() | 對象銷毀 (引用計數歸零、腳本結束) | 釋放資源 (關閉連接、句柄、鎖) | 無 |
__get(string $name) | 讀取不可訪問/不存在屬性 $obj->prop | 動態屬性、代理訪問、懶加載 | mixed |
__set(string $name, mixed $value) | 寫入不可訪問/不存在屬性 $obj->prop = $val | 數據驗證、日誌記錄、虛擬屬性 | 無 |
__isset(string $name) | isset($obj->prop) 或 empty($obj->prop) (不可訪問屬性) | 動態屬性存在性判斷 | bool |
__unset(string $name) | unset($obj->prop) (不可訪問屬性) | 清理動態屬性、緩存失效 | 無 |
__call(string $name, array $args) | 調用不可訪問/不存在實例方法 $obj->method() | 方法代理、動態方法、 fluent interface | mixed |
__callStatic(string $name, array $args) | 調用不可訪問/不存在靜態方法 Class::method() | 靜態工廠、門面模式、動態靜態代理 | mixed |
__sleep() | serialize($obj) 前 | 指定序列化屬性、關閉資源句柄 | array (屬性名列表) |
__wakeup() | unserialize($data) 後 | 重建連接、初始化狀態、修復引用 | 無 |
__serialize(): array | serialize($obj) (PHP 7.4+ 推薦替代 __sleep) | 精確控制序列化數據結構 | array (鍵值對) |
__unserialize(array $data): void | unserialize($data) (PHP 7.4+ 推薦替代 __wakeup) | 精確控制反序列化邏輯 | 無 |
__toString() | 對象作為字符串使用 echo $obj、(string)$obj | 調試輸出、模板渲染、日誌記錄 | string (拋出異常致命) |
__invoke(...$args) | 對象作為函數調用 $obj() | 可調用對象、策略執行器、閉包替代 | mixed |
__set_state(array $props): object | var_export($obj) 時 (靜態方法) | 導出可執行的重建代碼 | object (同類實例) |
__debugInfo(): array | var_dump($obj) (PHP 5.6+) | 控制調試輸出、隱藏敏感字段 | array (鍵值對) |
__clone() | clone $obj 後 | 深拷貝、重置 ID、複製資源句柄 | 無 |
class Model {
protected $attributes = [];
protected $original = [];
protected $casts = [];
public function __get(string $name) {
if (array_key_exists($name, $this->attributes)) {
$value = $this->attributes[$name];
// 類型轉換
return $this->castAttribute($name, $value);
}
// 關係懶加載
if (method_exists($this, $name)) {
return $this->$name(); // 調用關係方法
}
return null;
}
public function __set(string $name, $value): void {
$this->attributes[$name] = $value;
}
public function __isset(string $name): bool {
return array_key_exists($name, $this->attributes);
}
public function __unset(string $name): void {
unset($this->attributes[$name]);
}
protected function castAttribute(string $key, $value) {
$cast = $this->casts[$key] ?? null;
return match($cast) {
'int' => (int)$value,
'float' => (float)$value,
'bool' => (bool)$value,
'array' => json_decode($value, true),
'json' => json_decode($value, true),
'date' => DateTime::createFromFormat('Y-m-d', $value),
default => $value,
};
}
}
class User extends Model {
protected $casts = ['is_active' => 'bool', 'settings' => 'json'];
public function posts() { return $this->hasMany(Post::class); }
}
$user = new User();
$user->name = 'John'; // __set
echo $user->name; // __get
echo $user->is_active; // __get + cast bool
$user->posts(); // __get 觸發關係方法
class ServiceClient {
private $httpClient; // GuzzleHttp\Client (不可序列化)
private $config;
private $connected = false;
public function __construct(array $config) {
$this->config = $config;
}
// PHP 7.4+ 推薦
public function __serialize(): array {
return [
'config' => $this->config,
// 故意不序列化 $httpClient 和 $connected
];
}
public function __unserialize(array $data): void {
$this->config = $data['config'];
$this->connected = false;
$this->httpClient = null; // 反序列化時延遲重建
}
// 兼容舊版本
public function __sleep(): array {
return ['config'];
}
public function __wakeup(): void {
$this->connected = false;
$this->httpClient = null;
}
private function getClient() {
if (!$this->httpClient) {
$this->httpClient = new \GuzzleHttp\Client($this->config);
}
return $this->httpClient;
}
}
interface Strategy {
public function execute(mixed $input): mixed;
}
class StrategyExecutor {
private $strategies = [];
public function register(string $name, Strategy $strategy): void {
$this->strategies[$name] = $strategy;
}
// 讓對象可直接調用: $executor('strategy_name', $input)
public function __invoke(string $name, mixed $input): mixed {
if (!isset($this->strategies[$name])) {
throw new \InvalidArgumentException("Strategy not found: $name");
}
return $this->strategies[$name]->execute($input);
}
}
$executor = new StrategyExecutor();
$executor->register('upper', new class implements Strategy {
public function execute($input) { return strtoupper($input); }
});
$executor->register('reverse', new class implements Strategy {
public function execute($input) { return strrev($input); }
});
echo $executor('upper', 'hello'); // HELLO
echo $executor('reverse', 'world'); // dlrow
class ApiCredentials {
public string $apiKey;
public string $secretKey;
public string $endpoint;
public function __debugInfo(): array {
return [
'apiKey' => '****' . substr($this->apiKey, -4),
'secretKey' => '********',
'endpoint' => $this->endpoint,
];
}
}
$cred = new ApiCredentials();
$cred->apiKey = 'sk_live_abcdefghijklmnop';
$cred->secretKey = 'super_secret';
var_dump($cred);
/*
object(ApiCredentials)#1 (3) {
["apiKey"]=> string(20) "****mnop"
["secretKey"]=> string(8) "********"
["endpoint"]=> string(0) ""
}
*/
__get/__set 遞歸陷阱:在內部訪問 $this->name 會再次觸發 __get,導致無限遞歸。內部務必用 $this->attributes['name'] 或 parent::__get()。__sleep 返回非數組:會導致 serialize() 返回 null 並拋出 E_NOTICE。__wakeup 安全風險:反序列化可實例化任意類並觸發 __wakeup,導致 RCE (如 GuzzleHttp\Psr7\FnStream)。PHP 7.1+ 可用 allowed_classes 限制:unserialize($data, ['allowed_classes' => [MyClass::class]])。__toString 拋出異常:會觸發 E_RECOVERABLE_ERROR 並終止腳本,必須返回字符串。__debugInfo 性能:var_dump 大對象圖時會大量調用,避免在其中做複雜計算。__set_state:var_export 導出的代碼需 eval 執行,安全風險極大,慎用。parent::__construct()、parent::__destruct()、parent::__clone() 保證父類邏輯執行。| 常量 | 含義 | 示例輸出 |
|---|---|---|
__LINE__ | 當前行號 | 42 |
__FILE__ | 文件完整路徑 | /var/www/html/index.php |
__DIR__ | 文件所在目錄 | /var/www/html |
__FUNCTION__ | 函數名 | handleRequest |
__CLASS__ | 類名 (含命名空間) | App\Controllers\UserController |
__TRAIT__ | Trait 名 | App\Traits\Loggable |
__METHOD__ | 類方法名 | App\Controllers\UserController::handleRequest |
__NAMESPACE__ | 命名空間名 | App\Controllers |
魔術方法是 PHP 物件模型的「元編程入口」,合理使用能極大提升代碼表達力,濫用則導致難以維護的「魔法代碼」。遵循「顯式優於隱式」,僅在框架基礎設施、ORM、序列化、調試等基礎設施層使用。