PHP 類中以雙下劃線 __ 開頭的方法被稱為「魔術方法」。它們在特定生命周期或特定操作(如訪問不可見屬性、序列化、類型轉換)被自動觸發,讓開發者無需修改外部調用代碼即可擴展類行為。PHP 保留所有 __ 開頭的方法名,**自定義方法切勿使用此前綴**,除非確需覆寫魔術行為。
| 分類 | 方法 | 觸發時機 | 典型用途 |
|---|---|---|---|
| 構造/銷毀 | __construct() | 實例化 new Class() | 依賴注入、初始化資源、綁定事件 |
__destruct() | 對象銷毀(引用計數歸零、腳本結束) | 關閉連接、釋放鎖、寫入日志、緩存回寫 | |
| 屬性重載 | __get($name) | 讀取**不可見**屬性(不存在、private/protected) | 代理訪問、動態屬性、懶加載 |
__set($name, $value) | 寫入**不可見**屬性 | 數據驗證、日志記錄、關聯更新 | |
__isset($name) | isset($obj->prop) 或 empty() 檢查不可見屬性 | 配合 __get 實現完整語義 | |
__unset($name) | unset($obj->prop) 操作不可見屬性 | 清理代理數據、觸發側邊效應 | |
| 方法重載 | __call($name, $args) | 調用**不可見**實例方法 | 動態代理、API 客戶端、流式介面 |
__callStatic($name, $args) | 靜態調用**不可見**靜態方法(PHP 5.3+) | 工廠模式、查詢構建器、門面模式 | |
| 類型轉換 | __toString() | 對象作字符串使用(echo $obj、(string)$obj、printf('%s', $obj)) | 調試輸出、模板渲染、API 響應 |
| 調用邊界 | __invoke(...$args) | 對象作函數調用($obj()) | 閉包替代、可調用對象、中間件 |
| 克隆控制 | __clone() | clone $obj 後自動調用 | 深拷貝、重置資源 ID、斷開共享連接 |
| 序列化 | __sleep() | serialize($obj) 前 | 清理臨時數據、指定序列化白名單、關閉連接 |
__wakeup() | unserialize($data) 後 | 重建連接、初始化資源、驗證數據完整性 | |
| 現代序列化(PHP 7.4+) | __serialize() / __unserialize($data) | 取代 __sleep/__wakeup,支援類型聲明、繼承友好 | 新項目首選,避免父類私有屬性問題 |
| 導出 | __set_state($arr) | var_export($obj) 導出類時(static,PHP 5.1+) | 自定義導出格式、調試工具集成 |
| 自動加載 | __autoload() / spl_autoload_register() | 使用未定義類時(已棄用,請用 SPL) | 歷史項目兼容,新項目用 Composer PSR-4 |
class User {
private array $data = [];
private array $allowed = ['name', 'email', 'age'];
public function __set(string $name, $value): void {
if (!in_array($name, $this->allowed, true)) {
throw new InvalidArgumentException("屬性 $name 不允許寫入");
}
// 業務邏輯:email 格式驗證
if ($name === 'email' && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("郵箱格式無效");
}
$this->data[$name] = $value;
}
public function __get(string $name) {
if (!array_key_exists($name, $this->data)) {
throw new InvalidArgumentException("屬性 $name 不存在");
}
return $this->data[$name];
}
}
$user = new User();
$user->email = 'test@example.com'; // 觸發 __set 驗證
echo $user->email; // 觸發 __get
注意: __get 必須有返回值;__set 返回值被忽略。不可見包含「不存在」與「權限不足(private/protected)」兩種情況。
class QueryBuilder {
private string $table;
private array $wheres = [];
public function __construct(string $table) { $this->table = $table; }
// 非靜態:where('age', '>', 18)->where('status', 1)
public function __call(string $method, array $args) {
if ($method === 'where' && count($args) >= 2) {
[$col, $op, $val] = count($args) === 3 ? $args : [$args[0], '=', $args[1]];
$this->wheres[] = compact('col', 'op', 'val');
return $this; // 鏈式調用
}
throw new BadMethodCallError("方法 $method 不存在");
}
// 靜態:User::find(1) -> 實例化並查詢
public static function __callStatic(string $method, array $args) {
if ($method === 'find' && isset($args[0])) {
$instance = new static('users');
return $instance->where('id', $args[0])->first();
}
throw new BadMethodCallError("靜態方法 $method 不存在");
}
public function first() { /* 執行 SQL 返回單行 */ return null; }
}
// 使用
$qb = new QueryBuilder('posts');
$qb->where('status', 1)->where('views', '>', 1000);
User::find(42);
解決 __sleep 無法返回父類 private 屬性、繼承鏈混亂等歷史痛點。
class BaseModel {
private string $id;
protected DateTime $createdAt;
public function __serialize(): array {
return [
'id' => $this->id,
'created_at' => $this->createdAt->format('c'),
];
}
public function __unserialize(array $data): void {
$this->id = $data['id'];
$this->createdAt = new DateTime($data['created_at']);
}
}
class User extends BaseModel {
private string $email;
public function __serialize(): array {
return array_merge(parent::__serialize(), [
'email' => $this->email,
]);
}
public function __unserialize(array $data): void {
parent::__unserialize($data);
$this->email = $data['email'];
}
}
class RateLimiter {
private int $max;
private int $window;
private array $hits = [];
public function __construct(int $max, int $windowSeconds) {
$this->max = $max;
$this->window = $windowSeconds;
}
public function __invoke(string $key): bool {
$now = time();
$this->hits[$key] = array_filter($this->hits[$key] ?? [], fn($t) => $now - $t < $this->window);
if (count($this->hits[$key]) >= $this->max) return false;
$this->hits[$key][] = $now;
return true;
}
}
$limiter = new RateLimiter(5, 60); // 每分鐘 5 次
if ($limiter('api:user:123')) { /* 允許 */ } else { /* 限流 */ }
E_RECOVERABLE_ERROR;不能拋出異常,會直接 Fatal Error。__serialize)。try-catch 內部捕獲。spl_autoload_register魔術方法是 PHP 物件模型的「擴展插槽」,用好能極大提升代碼表達力(如 ORM、API Client、DSL 構建);濫用則導致隱式行為難追蹤。原則:**只在框架層、基礎設施層、領域模型邊界使用;業務邏輯層盡量顯式調用**。掌握 15 個鉤子的觸發時機與契約,是進階 PHP 工程師的必修課。