PHP 中以雙底線 __ 開頭的方法稱為魔術方法,它們在特定情況下會被自動觸發,無需手動呼叫。掌握這些方法能讓類別行為更直觀、API 更優雅。
| 方法 | 觸發時機 | 典型用途 |
|---|---|---|
__construct() | 實例化 new Class() | 初始化屬性、注入依賴、建立連線 |
__destruct() | 物件銷毀 (引用計數歸零、腳本結束) | 釋放資源、關閉連線、寫入日誌 |
__call($name, $args) | 呼叫不可存取/不存在的實例方法 | 方法代理、動態方法、流暢介面 |
__callStatic($name, $args) | 呼叫不可存取/不存在的靜態方法 | 靜態工廠、動態查詢建構器 |
__get($name) | 讀取不可存取/不存在的屬性 | 虛擬屬性、延遲載入、委派存取 |
__set($name, $value) | 寫入不可存取/不存在的屬性 | 屬性驗證、觸發事件、同步快取 |
__isset($name) | 對不可存取屬性呼叫 isset()/empty() | 虛擬屬性存在性檢查 |
__unset($name) | 對不可存取屬性呼叫 unset() | 清理虛擬屬性 |
__toString() | 物件被當作字串使用 (echo, print, 字串串接) | 物件序列化顯示、除錯輸出 |
__invoke() | 物件被當作函式呼叫 $obj() | 可呼叫物件、閉包替代、策略模式 |
__clone() | 執行 clone $obj | 深層複製、重置識別碼、複製相關資源 |
__sleep() | serialize($obj) | 指定序列化屬性、關閉資源 |
__wakeup() | unserialize($data) | 重建連線、初始化資源 |
__serialize() | serialize($obj) (PHP 7.4+) | 自定義序列化資料陣列 |
__unserialize() | unserialize($data) (PHP 7.4+) | 自定義反序列化邏輯 |
__debugInfo() | var_dump($obj) | 自訂除錯輸出內容 |
<?php
class Person {
public string $name;
protected string $sex;
private float $salary;
// 1. 建構子
public function __construct(string $name, string $sex, float $salary) {
$this->name = $name;
$this->sex = $sex;
$this->salary = $salary;
echo "建構子執行: $name 入職\n";
}
// 2. 解構子
public function __destruct() {
echo "解構子執行: $this->name 离職\n";
}
// 3. 字串轉換
public function __toString(): string {
return "Person[名字:$this->name, 性別:$this->sex]";
}
// 4. 複製
public function __clone() {
echo "__clone 觸發: 建立 $this->name 的副本\n";
$this->name .= '_copy'; // 副本標記
$this->salary = 0; // 重置薪資
}
// 5. 讀取不可存取屬性
public function __get(string $name) {
echo "__get 呼叫: 嘗試讀取 '$name'\n";
if ($name === 'salary') {
return '***** (隱私)';
}
return "屬性 '$name' 不存在或無權限";
}
// 6. 寫入不可存取屬性
public function __set(string $name, $value) {
echo "__set 呼叫: 嘗試設定 '$name' = '$value'\n";
if ($name === 'salary') {
throw new Exception('薪資不可直接修改,請用 adjustSalary()');
}
$this->$name = $value; // 動態建立公開屬性
}
// 7. 檢查不可存取屬性是否存在
public function __isset(string $name): bool {
echo "__isset 呼叫: 檢查 '$name'\n";
return in_array($name, ['name', 'sex', 'salary'], true);
}
// 8. 呼叫不存在方法
public function __call(string $name, array $args) {
echo "__call 呼叫: 方法 '$name' 不存在,參數: " . json_encode($args) . "\n";
if ($name === 'getName') {
return $this->name;
}
throw new BadMethodCallException("方法 $name 不存在");
}
// 9. 靜態方法代理
public static function __callStatic(string $name, array $args) {
echo "__callStatic 呼叫: 靜態方法 '$name'\n";
if ($name === 'create') {
return new self(...$args);
}
throw new BadMethodCallException("靜態方法 $name 不存在");
}
// 10. 可呼叫物件
public function __invoke(string $greeting = 'Hello') {
return "$greeting, $this->name!";
}
// 11. 序列化控制 (PHP 7.4+ 推薦用 __serialize)
public function __sleep(): array {
return ['name', 'sex']; // 不序列化 salary
}
public function __wakeup() {
$this->salary = 0; // 反序列化後重置
}
// 12. 除錯資訊
public function __debugInfo(): array {
return [
'name' => $this->name,
'sex' => $this->sex,
'salary' => '******',
];
}
// 一般方法
public function adjustSalary(float $amount): void {
$this->salary += $amount;
}
}
// --- 測試程式碼 ---
echo "=== 1. 實例化 ===\n";
$p = new Person('張三', '男', 50000);
echo "\n=== 2. 字串轉換 ===\n";
echo $p; // 自動呼叫 __toString()
echo "\n=== 3. 複製 ===\n";
$p2 = clone $p;
echo "複製後名字: $p2->name\n";
echo "\n=== 4. 存取私有屬性 ===\n";
echo "薪資: " . $p->salary . "\n"; // 觸發 __get
echo "\n=== 5. 設定不存在屬性 ===\n";
$p->department = 'RD'; // 觸發 __set,動態建立 public 屬性
echo "部門: $p->department\n";
echo "\n=== 6. 檢查屬性存在 ===\n";
var_dump(isset($p->salary)); // 觸發 __isset
echo "\n=== 7. 呼叫不存在方法 ===\n";
echo "名字: " . $p->getName() . "\n"; // 觸發 __call
echo "\n=== 8. 靜態工廠 ===\n";
$p3 = Person::create('李四', '女', 60000); // 觸發 __callStatic
echo "\n=== 9. 可呼叫物件 ===\n";
echo $p('早安') . "\n"; // 觸發 __invoke
echo "\n=== 10. 序列化 ===\n";
$ser = serialize($p);
echo "序列化: $ser\n";
$p4 = unserialize($ser);
echo "反序列化後薪資: " . $p4->salary . "\n";
echo "\n=== 11. 除錯輸出 ===\n";
var_dump($p);
=== 1. 實例化 ===
建構子執行: 張三 入職
=== 2. 字串轉換 ===
Person[名字:張三, 性別:男]
=== 3. 複製 ===
__clone 觸發: 建立 張三 的副本
複製後名字: 張三_copy
=== 4. 存取私有屬性 ===
__get 呼叫: 嘗試讀取 'salary'
薪資: ***** (隱私)
=== 5. 設定不存在屬性 ===
__set 呼叫: 嘗試設定 'department' = 'RD'
部門: RD
=== 6. 檢查屬性存在 ===
__isset 呼叫: 檢查 'salary'
bool(true)
=== 7. 呼叫不存在方法 ===
__call 呼叫: 方法 'getName' 不存在,參數: []
名字: 張三
=== 8. 靜態工廠 ===
__callStatic 呼叫: 靜態方法 'create'
建構子執行: 李四 入職
=== 9. 可呼叫物件 ===
早安, 張三!
=== 10. 序列化 ===
序列化: O:6:"Person":2:{s:4:"name";s:6:"張三";s:3:"sex";s:3:"男";}
反序列化後薪資: 0
=== 11. 除錯輸出 ===
object(Person)#1 (3) {
["name"]=> string(6) "張三"
["sex"]=> string(3) "男"
["salary"]=> string(6) "******"
}
method_exists。clone $this->obj。'\0ClassName\0prop'。Stringable (PHP 8.0+) 介面明確宣告支援 __toString:class Person implements Stringable。$config->get('database.host')。$query->whereName('John')->orderBy('id')。魔術方法是 PHP 物件導向進階開發的核心工具,善用它們能寫出極具表達力、符合直覺的 API,但也需謹慎平衡「魔法」與「明確」的界線。