封裝性要求屬性設為 private,但頻繁撰寫 getName()/setName() 既冗長又僵化。PHP 5 提供四大魔術方法:__get、__set、__isset、__unset,實現**私有屬性的透明讀寫、存在性檢查與刪除**,讓物件操作語法如同公有屬性般自然。
| 方法 | 觸發時機 | 參數 | 回傳值 | 典型用途 |
|---|---|---|---|---|
__get($name) | 讀取不可存取屬性(private/不存在) | $name:屬性名 | 屬性值或 null | 透明讀取、虛擬屬性、委派存取 |
__set($name, $value) | 寫入不可存取屬性 | $name, $value | 無(void) | 透明寫入、驗證、日誌、觸發事件 |
__isset($name) | isset($obj->prop) 或 empty() | $name | bool | 私有屬性存在性檢查 |
__unset($name) | unset($obj->prop) | $name | 無 | 私有屬性刪除、清理關聯資源 |
<?php
class Person {
private $name, $sex, $age;
// 透明讀取
public function __get($property_name) {
echo "__get() 自動調用:讀取 $property_name\n";
return isset($this->$property_name) ? $this->$property_name : null;
}
// 透明寫入(可加驗證)
public function __set($property_name, $value) {
echo "__set() 自動調用:設定 $property_name = $value\n";
// 例:年齡必須為正整數
if ($property_name === 'age' && (!is_int($value) || $value < 0)) {
throw new InvalidArgumentException('年齡必須為非負整數');
}
$this->$property_name = $value;
}
// isset()/empty() 支援
public function __isset($nm) {
echo "__isset() 自動調用:檢查 $nm\n";
return isset($this->$nm);
}
// unset() 支援
public function __unset($nm) {
echo "__unset() 自動調用:刪除 $nm\n";
unset($this->$nm);
}
}
// 測試
$p1 = new Person();
$p1->name = '張三'; // 觸發 __set
$p1->sex = '男';
$p1->age = 20;
echo "姓名:{$p1->name}\n"; // 觸發 __get
echo "性別:{$p1->sex}\n";
echo "年齡:{$p1->age}\n";
var_dump(isset($p1->name)); // 觸發 __isset → bool(true)
unset($p1->name); // 觸發 __unset
var_dump(isset($p1->name)); // 觸發 __isset → bool(false)
echo "刪除後姓名:{$p1->name}\n"; // __get 回傳 null
?>
__set() 自動調用:設定 name = 張三
__set() 自動調用:設定 sex = 男
__set() 自動調用:設定 age = 20
__get() 自動調用:讀取 name
姓名:張三
__get() 自動調用:讀取 sex
性別:男
__get() 自動調用:讀取 age
年齡:20
__isset() 自動調用:檢查 name
bool(true)
__unset() 自動調用:刪除 name
__isset() 自動調用:檢查 name
bool(false)
__get() 自動調用:讀取 name
刪除後姓名:
public function __get($name) {
if ($name === 'full_name') return $this->first_name . ' ' . $this->last_name;
if ($name === 'is_adult') return $this->age >= 18;
return isset($this->$name) ? $this->$name : null;
}
public function __get($name) {
if (!isset($this->data[$name])) {
$this->data[$name] = $this->db->query("SELECT $name FROM users WHERE id=?", $this->id);
}
return $this->data[$name];
}
public function __set($name, $value) {
$old = $this->$name ?? null;
$this->$name = $value;
if ($old !== $value) $this->triggerEvent('changed', [$name, $old, $value]);
}
| 誤區 | 正確做法 |
|---|---|
魔術方法必須是 public | 建議 public,但 protected 也可運作(僅限繼承類別觸發) |
| 用來替代所有 getter/setter | 僅用於動態/虛擬屬性;固定屬性仍建議显式方法(IDE 提示、型別提示、文檔化) |
__get 回傳引用 | PHP 不支援魔術方法回傳引用,需用 &__get()(罕用) |
| 遞迴調用導致無限循環 | 內部存取私有屬性用 $this->data[$name] 而非 $this->$name |
| 效能開銷大 | 現代 PHP (7.4+) 優化極好;熱點路徑可用显式方法並搭配魔術方法作為 fallback |
__call($name, $args) / __callStatic():動態方法呼叫__sleep() / __wakeup():序列化控制__toString():物件轉字串__invoke():物件當函數呼叫__debugInfo():var_dump() 自訂輸出__set/__get/__isset/__unset 是 PHP 物件導向的「語法糖」,讓封裝與便利性並存。善用它們實現虛擬屬性、資料委派、驗證攔截、變更通知等進階模式,能大幅提升程式碼表達力。但切記:**显式優於隱式**,核心業務屬性仍以標準 getter/setter 為主,魔術方法作為補完與擴充點。