PHP 提供了一組「魔術方法」(Magic Methods),常被稱為「攔截器」,它們在物件生命週期的特定時刻自動觸發,讓開發者能優雅地處理未定義屬性/方法的存取、設定、檢查與銷毀。本文系統梳理五大核心攔截器,並附上完整可執行的實戰範例。
| 方法 | 觸發時機 | 典型用途 |
|---|---|---|
__get($property) | 讀取不可見/不存在屬性時 | 動態屬性代理、延遲載入、只讀屬性 |
__set($property, $value) | 寫入不可見/不存在屬性時 | 屬性驗證、自動轉換、觸發副作用 |
__isset($property) | 對不可見屬性呼叫 isset() 或 empty() | 自定義存在性判斷邏輯 |
__unset($property) | 對不可見屬性呼叫 unset() | 資源釋放、快取清理、軟刪除標記 |
__call($method, $args) | 呼叫不可見/不存在方法時 | 動態方法代理、API 封裝、流式介面 |
以下類別示範如何將所有動態屬性操作轉發給對應的 getXxx()、setXxx()、issetXxx()、unsetXxx() 方法,實現層級封裝:
class InterceptDemo {
private $xingming = '';
private $age = 10;
// 讀取未定義屬性 -> 呼叫 get{Property}()
public function __get($property) {
$method = 'get' . ucfirst($property);
if (method_exists($this, $method)) {
return $this->$method();
}
trigger_error("屬性 '$property' 不存在且無對應 getter", E_USER_NOTICE);
return null;
}
// 寫入未定義屬性 -> 呼叫 set{Property}($value)
public function __set($property, $value) {
$method = 'set' . ucfirst($property);
if (method_exists($this, $method)) {
return $this->$method($value);
}
trigger_error("屬性 '$property' 不存在且無對應 setter", E_USER_NOTICE);
}
// isset/empty 檢查 -> 呼叫 isset{Property}()
public function __isset($property) {
$method = 'isset' . ucfirst($property);
if (method_exists($this, $method)) {
return $this->$method();
}
return false;
}
// unset 操作 -> 呼叫 unset{Property}()
public function __unset($property) {
$method = 'unset' . ucfirst($property);
if (method_exists($this, $method)) {
return $this->$method();
}
}
// 呼叫未定義方法 -> 簡易 getXxx() 代理
public function __call($method, $args) {
if (strpos($method, 'get') === 0) {
$prop = lcfirst(substr($method, 3));
if (property_exists($this, $prop)) {
return $this->$prop;
}
}
throw new BadMethodCallException("方法 '$method' 不存在");
}
// ---- 具體業務方法 ----
public function getName() { return $this->xingming; }
public function setName($v) { $this->xingming = $v; }
public function issetName() { return !is_null($this->xingming); }
public function unsetName() { $this->xingming = null; }
public function testIsset() { return isset($this->Name); }
}
$obj = new InterceptDemo();
echo "設定 Name = 'Li':\n";
$obj->Name = 'Li'; // 觸發 __set -> setName('Li')
echo "讀取 Name:{$obj->Name}\n"; // 觸發 __get -> getName()
echo "isset(Name):" . ($obj->testIsset() ? 'true' : 'false') . "\n"; // 觸發 __isset -> issetName()
echo "unset(Name) 後:\n";
unset($obj->Name); // 觸發 __unset -> unsetName()
echo "Name = {$obj->Name}\n"; // 觸發 __get -> getName() 回傳 null
echo "呼叫未定義 getAge():\n";
echo "Age = {$obj->getAge()}\n"; // 觸發 __call -> 回傳 $this->age (10)
__get/__set 內部存取同名屬性會再次觸發攔截器,導致無限遞迴,務必操作 private/protected 實際儲存屬性。#[\ReturnTypeWillChange] 或明確宣告回傳型別,提升靜態分析準確度。__call 轉發,但建議改用 ArrayAccess 或 PropertyAccess 元件(如 Symfony PropertyAccess)。__sleep/__wakeup 時需注意動態屬性是否需持久化。__get/__set 提供點號存取語法。__call 動態建構 HTTP 請求(如 $client->getUser(1) → GET /users/1)。PHP 魔術攔截器是元編程利器,善用能大幅減少樣板碼、提升 API 表達力。但濫用會導致程式碼難以追蹤、IDE 自動完成失效、效能下降。建議:核心領域模型優先定義明確屬性/方法,邊界層(DTO、Proxy、Facade)再靈活運用攔截器。