PHP 魔術方法是以雙底線開頭的特殊方法,在特定情況下自動被呼叫。掌握這些方法能讓你寫出更靈活、更符合物件導向原則的程式碼。本文深入解析核心魔術方法與物件複製行為差異。
| 方法 | 觸發時機 | 常見用途 |
|---|---|---|
__construct() | 物件實例化時 | 初始化屬性、資源連線 |
__destruct() | 物件銷毀時 | 釋放資源、關閉連線 |
__call($name, $args) | 呼叫不可存取的方法 | 動態方法、代理模式 |
__callStatic($name, $args) | 靜態呼叫不可存取方法 | 流暢介面、工廠方法 |
__get($name) | 讀取不可存取屬性 | 唯讀屬性、延遲載入 |
__set($name, $value) | 寫入不可存取屬性 | 屬性驗證、自動轉型 |
__isset($name) | isset()/empty() 檢查不可存取屬性 | 虛擬屬性存在性判斷 |
__unset($name) | unset() 不可存取屬性 | 清理相關資源 |
__sleep() | serialize() 序列化前 | 指定需序列化的屬性 |
__wakeup() | unserialize() 反序列化後 | 重新建立資源連線 |
__serialize() | PHP 7.4+ 序列化 | 自訂序列化資料結構 |
__unserialize() | PHP 7.4+ 反序列化 | 自訂反序列化邏輯 |
__toString() | 物件當字串使用時 | 除錯輸出、模板渲染 |
__invoke() | 物件當函數呼叫時 | 可呼叫物件、閉包替代 |
__clone() | clone 物件時 | 深層複製、資源重新建立 |
__debugInfo() | var_dump() 物件時 | 控制除錯輸出內容 |
class Computer {
public $name = 'Default';
public function __construct() { echo "建立新電腦\n"; }
}
$computer1 = new Computer(); // 建立新電腦
$computer1->name = 'MacBook';
$computer2 = new Computer(); // 建立另一台新電腦
$computer2->name = 'ThinkPad';
echo $computer1->name; // MacBook
echo $computer2->name; // ThinkPad
// 兩個物件完全獨立,互不影響
$computer3 = new Computer(); // 建立新電腦 $computer3->name = 'MacBook'; $computer4 = $computer3; // 參考賦值,指向同一記憶體 $computer4->name = 'ThinkPad'; echo $computer3->name; // ThinkPad (被改變!) echo $computer4->name; // ThinkPad // $computer3 和 $computer4 指向同一個物件實例
class Computer {
public $name = 'Default';
public $specs = ['cpu' => 'i7', 'ram' => '16GB']; // 陣列屬性
public $monitor = null; // 物件屬性
public function __clone() {
// 深層複製陣列屬性
$this->specs = $this->specs; // 陣列複製觸發 copy-on-write
// 若有物件屬性需手動 clone
// if ($this->monitor) $this->monitor = clone $this->monitor;
}
}
$computer5 = new Computer();
$computer5->name = 'MacBook Pro';
$computer5->specs['cpu'] = 'M2';
$computer6 = clone $computer5; // 呼叫 __clone()
$computer6->name = 'MacBook Air';
$computer6->specs['cpu'] = 'M1';
echo $computer5->name; // MacBook Pro (未變)
echo $computer5->specs['cpu']; // M2 (未變,因 __clone 處理了陣列)
echo $computer6->name; // MacBook Air
echo $computer6->specs['cpu']; // M1
| 方式 | 語法 | 記憶體 | 標量屬性 | 參考型屬性 (陣列/物件) |
|---|---|---|---|---|
| 新實例 | new Class() | 新 | 獨立 | 獨立 |
| 參考賦值 | $b = $a | 共享 | 共享 | 共享 |
| Clone (無 __clone) | clone $a | 新 | 獨立 | 共享 (淺層複製) |
| Clone (有 __clone) | clone $a | 新 | 獨立 | 依 __clone 實作 (可深層複製) |
class User {
private $data = [];
private $allowed = ['name', 'email', 'age'];
public function __set($name, $value) {
if (in_array($name, $this->allowed)) {
// 驗證邏輯
if ($name === 'age' && (!is_int($value) || $value < 0)) {
throw new InvalidArgumentException('年齡必須為正整數');
}
if ($name === 'email' && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Email 格式錯誤');
}
$this->data[$name] = $value;
} else {
throw new Exception("屬性 {$name} 不允許設定");
}
}
public function __get($name) {
return $this->data[$name] ?? null;
}
public function __isset($name) {
return isset($this->data[$name]);
}
public function __unset($name) {
unset($this->data[$name]);
}
}
$user = new User();
$user->name = '張三'; // 觸發 __set
$user->age = 25;
echo $user->name; // 觸發 __get
echo isset($user->email); // 觸發 __isset (false)
unset($user->age); // 觸發 __unset
class Database {
private $pdo;
public function __call($method, $args) {
// 動態處理 findBy* 方法
if (preg_match('/^findBy(.+)$/', $method, $matches)) {
$field = lcfirst($matches[1]); // findByEmail -> email
return $this->findByField($field, $args[0] ?? null);
}
throw new BadMethodCallException("方法 {$method} 不存在");
}
public static function __callStatic($method, $args) {
if ($method === 'connect') {
return new static(); // 工廠模式
}
throw new BadMethodCallException("靜態方法 {$method} 不存在");
}
private function findByField($field, $value) {
// 實際查詢邏輯
return "SELECT * FROM users WHERE {$field} = {$value}";
}
}
$db = Database::connect(); // 觸發 __callStatic
echo $db->findByEmail('test@test.com'); // 觸發 __call
class RequestLogger {
private $logs = [];
public function __invoke($request) {
$this->logs[] = [
'time' => microtime(true),
'url' => $request->getUrl(),
'method' => $request->getMethod()
];
return $this; // 支援鏈式呼叫
}
public function getLogs() {
return $this->logs;
}
}
$logger = new RequestLogger();
$logger($request1)($request2)($request3); // 連續呼叫
print_r($logger->getLogs());
// 舊式 (已棄用)
function __autoload($class) {
include $class . '.php';
}
// 現代標準做法
spl_autoload_register(function ($class) {
$file = __DIR__ . '/' . str_replace('\\', '/', $class) . '.php';
if (file_exists($file)) {
require $file;
}
});
// 或使用 Composer 自動載入 (推薦)
require __DIR__ . '/vendor/autoload.php';
PHP 魔術方法提供了強大的物件生命週期控制能力。重點掌握:
__debugInfo() 避免洩露敏感資料善用魔術方法能讓 API 設計更優雅,但濫用會降低程式碼可讀性,請在適當場合使用。