魔術方法是以雙底線 __ 開頭的特殊方法,PHP 在特定生命週期或語法情境下自動觸發。它們屬於「語法糖」,不新增語言功能,但大幅提升程式碼表達力與靈活度。
| 方法 | 觸發時機 | 典型用途 |
|---|---|---|
__construct | 物件實例化 new Class() | 初始化屬性、注入依賴、建立連線 |
__destruct | 物件銷毀(腳本結束、unset、引用歸零) | 釋放資源、關閉連線、寫入日誌 |
__call | 呼叫不可存取的實例方法 | 動態代理、方法不存在時的 fallback、DSL 建構 |
__callStatic | 呼叫不可存取的靜態方法 | 靜態代理、流式介面靜態入口 |
__get | 讀取不可存取的屬性 | 虛擬屬性、資料轉換、懶惰載入 |
__set | 寫入不可存取的屬性 | 驗證、記錄變更、關聯更新 |
__isset | 對不可存取屬性執行 isset()/empty() | 配合 __get 實作虛擬屬性存在性判斷 |
__unset | 對不可存取屬性執行 unset() | 清理虛擬屬性、觸發事件 |
__sleep | serialize() 前 | 指定需序列化的屬性、關閉資源連線 |
__wakeup | unserialize() 後 | 重建資源連線、初始化狀態 |
__toString | 物件被當作字串使用(echo、print、字串串接) | 物件友善輸出、日誌記錄 |
__invoke | 物件被當作函式呼叫 $obj() | 可呼叫物件、策略模式、中介軟體 |
__set_state | var_export() 匯出類別時 | 自定義匯出陣列結構,支援 eval() 還原 |
__clone | clone $obj 時 | 深層複製、重設識別碼、複製相關資源 |
__debugInfo | var_dump() 顯示物件時 | 控制除錯輸出內容,隱藏敏感資訊 |
class Database {
private $pdo;
public function __construct(string $dsn, string $user, string $pass) {
$this->pdo = new PDO($dsn, $user, $pass);
}
public function __destruct() {
$this->pdo = null; // 關閉連線
}
}
class Entity {
private array $data = [];
private array $changed = [];
public function __get(string $name) {
return $this->data[$name] ?? null;
}
public function __set(string $name, $value): void {
$this->data[$name] = $value;
$this->changed[$name] = true;
}
public function __isset(string $name): bool {
return isset($this->data[$name]);
}
public function __unset(string $name): void {
unset($this->data[$name], $this->changed[$name]);
}
public function getDirty(): array { return array_keys($this->changed); }
}
$e = new Entity();
$e->title = 'Hello'; // 觸發 __set
echo $e->title; // 觸發 __get
var_dump(isset($e->title)); // 觸發 __isset
unset($e->title); // 觸發 __unset
class Router {
public function __call(string $method, array $args) {
// 動態處理 getUsers、postLogin 等
$verb = strtolower(substr($method, 0, 3)); // get/post/put/delete
$action = lcfirst(substr($method, 3));
echo "Route: $verb $action" . PHP_EOL;
}
public static function __callStatic(string $method, array $args) {
echo "Static route: $method" . PHP_EOL;
}
}
$r = new Router();
$r->getUsers(); // Route: get users
Router::dispatch(); // Static route: dispatch
class UserSession {
public $userId;
public $token;
private $pdo; // 資料庫連線不可序列化
public function __sleep(): array {
return ['userId', 'token']; // 僅序列化這兩個屬性
}
public function __wakeup(): void {
$this->pdo = new PDO('mysql:host=localhost;dbname=app', 'user', 'pass');
}
}
class Money {
public function __construct(public float $amount, public string $currency = 'TWD') {}
public function __toString(): string {
return number_format($this->amount, 2) . ' ' . $this->currency;
}
}
$price = new Money(1299.5);
echo "價格:$price"; // 價格:1,299.50 TWD
class Logger {
public function __invoke(string $level, string $message): void {
echo "[$level] " . date('Y-m-d H:i:s') . " $message" . PHP_EOL;
}
}
$log = new Logger();
$log('INFO', 'Application started'); // [INFO] 2024-01-15 10:30:00 Application started
class Order {
public $id;
public $items = [];
public function __clone() {
$this->id = uniqid('ord_'); // 新訂單編號
// 深層複製商品陣列
$this->items = array_map(fn($item) => clone $item, $this->items);
}
}
class ApiKey {
public function __construct(public string $key) {}
public function __debugInfo(): array {
return ['key' => '****' . substr($this->key, -4)]; // 隱藏大部分金鑰
}
}
var_dump(new ApiKey('sk_live_abcdef123456'));
// object(ApiKey)#1 (1) { ["key"]=> string(10) "****123456" }
舊式:全域函式 __autoload($class),只能定義一個,已棄用。
新式:spl_autoload_register(callable $callback) 可註冊多個載入器,支援類別靜態方法、閉包、多命名空間對應。
spl_autoload_register(function(string $class) {
$file = __DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php';
if (file_exists($file)) require $file;
});
__toString 必須回傳 string,否則拋出 Error。public,__callStatic 亦需 static。__autoload 拋出的例外無法被 catch 捕獲,會導致致命錯誤;改用 spl_autoload_register 並內部 try-catch。__sleep 排除不需持久化的資料,減少體積。__serialize() / __unserialize() 取代 __sleep/__wakeup,提供更細粒度控制。魔術方法是 PHP 物件模型的靈魂,善用它們能實作 ActiveRecord、資料傳輸物件(DTO)、中介軟體、流式介面等優雅模式。但濫用會降低可讀性,建議:「核心領域模型慎用,基礎設施層大膽用」。