| 方法 | 觸發時機 | 參數 | 回傳值要求 |
__construct() | 物件實例化時 (`new Class()`) | 任意 | 無 (void) |
__destruct() | 物件銷毀時 (引用計數歸零、腳本結束) | 無 | 無 |
__call() | 呼叫不可存取/不存在的實例方法 | `string $name, array $arguments` | 任意 (呼叫者將收到) |
__callStatic() | 呼叫不可存取/不存在的靜態方法 | `string $name, array $arguments` | 任意 |
__get() | 讀取不可存取/不存在的屬性 | `string $name` | 任意 |
__set() | 寫入不可存取/不存在的屬性 | `string $name, mixed $value` | 無 |
__isset() | 對不可存取屬性呼叫 `isset()`/`empty()` | `string $name` | `bool` |
__unset() | 對不可存取屬性呼叫 `unset()` | `string $name` | 無 |
__sleep() | `serialize($obj)` 前 | 無 | `array` (需序列化的屬性名稱陣列) |
__wakeup() | `unserialize($data)` 後 | 無 | 無 |
__serialize() | `serialize($obj)` (PHP 7.4+ 優先) | 無 | `array` (鍵值對陣列) |
__unserialize() | `unserialize($data)` (PHP 7.4+ 優先) | `array $data` | 無 |
__toString() | 物件被當作字串使用 (`echo $obj`, `(string)$obj`) | 無 | `string` (否則拋出 Error) |
__invoke() | 物件被當作函數呼叫 (`$obj()`) | 任意 | 任意 |
__set_state() | `var_export($obj)` 時 (static) | `array $properties` | 物件實例 |
__clone() | `clone $obj` 後 | 無 | 無 |
__debugInfo() | `var_dump($obj)` 時 (PHP 5.6+) | 無 | `array` (顯示的屬性鍵值對) |
注意:`__sleep`/`__wakeup` 為舊版序列化介面,PHP 7.4+ 建議使用 `__serialize`/`__unserialize` (支援型別、避免兼容性問題)。
詳細範例與應用場景
1. 建構子與解構子:資源管理
<?php
class DatabaseConnection
{
private PDO $pdo;
private string $dsn;
public function __construct(string $dsn, string $user, string $pass)
{
$this->dsn = $dsn;
$this->pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
echo "[DB] 連線建立: $dsn\n";
}
public function query(string $sql, array $params = []): array
{
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
public function __destruct()
{
// PDO 會自動關閉,這裡示範額外清理邏輯
$this->pdo = null;
echo "[DB] 連線關閉: {$this->dsn}\n";
}
}
// 使用
$db = new DatabaseConnection('mysql:host=localhost;dbname=test', 'root', '');
$users = $db->query('SELECT * FROM users LIMIT 5');
// 腳本結束或 $db 超出作用域時自動呼叫 __destruct
2. `__call` / `__callStatic`:動態方法與代理
<?php
class ApiClient
{
private string $baseUrl;
private array $endpoints = ['users', 'posts', 'comments'];
public function __construct(string $baseUrl) { $this->baseUrl = $baseUrl; }
// 動態建立 $client->users()->get() 等鏈式呼叫
public function __call(string $name, array $arguments)
{
if (in_array($name, $this->endpoints)) {
return new EndpointProxy($this->baseUrl . "/$name");
}
throw new BadMethodCallException("Endpoint '$name' 不存在");
}
// 靜態快捷呼叫:ApiClient::users()->get()
public static function __callStatic(string $name, array $arguments)
{
if (in_array($name, (new self(''))->endpoints)) {
return (new self('https://api.example.com'))->$name(...$arguments);
}
throw new BadMethodCallException("Static endpoint '$name' 不存在");
}
}
class EndpointProxy
{
private string $url;
public function __construct(string $url) { $this->url = $url; }
public function get(array $query = []): array
{
$url = $this->url . '?' . http_build_query($query);
return json_decode(file_get_contents($url), true) ?? [];
}
public function post(array $data): array { /* ... */ }
}
// 使用
$client = new ApiClient('https://jsonplaceholder.typicode.com');
$users = $client->users()->get(['limit' => 5]); // 觸發 __call('users') -> EndpointProxy->get()
$posts = ApiClient::posts()->get(); // 觸發 __callStatic('posts')
3. `__get` / `__set` / `__isset` / `__unset`:屬性重載與虛擬屬性
<?php
class UserModel
{
private array $data = [];
private array $dirty = []; // 追蹤異動欄位
// 只允許存取白名單欄位
private array $fillable = ['name', 'email', 'password', 'avatar'];
public function __get(string $name)
{
return $this->data[$name] ?? null;
}
public function __set(string $name, mixed $value): void
{
if (!in_array($name, $this->fillable)) {
throw new Exception("欄位 '$name' 不允許寫入");
}
$this->data[$name] = $value;
$this->dirty[$name] = true;
}
public function __isset(string $name): bool
{
return isset($this->data[$name]);
}
public function __unset(string $name): void
{
unset($this->data[$name], $this->dirty[$name]);
}
public function getDirty(): array { return array_keys($this->dirty); }
public function toArray(): array { return $this->data; }
}
$user = new UserModel();
$user->name = 'John'; // 觸發 __set
$user->email = 'john@example.com';
echo $user->name; // 觸發 __get
isset($user->name); // true, 觸發 __isset
unset($user->name); // 觸發 __unset
echo $user->getDirty(); // ['name', 'email']
4. `__sleep` / `__wakeup` / `__serialize` / `__unserialize`:序列化控制
<?php
class SessionData
{
public string $userId;
public array $permissions = [];
private PDO $db; // 資源不可序列化
private string $cacheKey;
// PHP 7.4+ 建議使用 __serialize/__unserialize
public function __serialize(): array
{
return [
'userId' => $this->userId,
'permissions' => $this->permissions,
'cacheKey' => $this->cacheKey,
];
}
public function __unserialize(array $data): void
{
$this->userId = $data['userId'];
$this->permissions = $data['permissions'];
$this->cacheKey = $data['cacheKey'];
// 反序列化後重新建立資源連線
$this->db = new PDO('mysql:host=localhost;dbname=test', 'root', '');
}
// 舊版相容 (PHP < 7.4)
public function __sleep(): array
{
return ['userId', 'permissions', 'cacheKey']; // 排除 $db
}
public function __wakeup(): void
{
$this->db = new PDO('mysql:host=localhost;dbname=test', 'root', '');
}
}
5. `__toString`:物件字串化
<?php
class Money
{
public function __construct(
public int $amount, // 分
public string $currency = 'TWD'
) {}
public function __toString(): string
{
return sprintf('%.2f %s', $this->amount / 100, $this->currency);
}
}
$price = new Money(19900, 'TWD');
echo "價格: $price"; // 自動呼叫 __toString -> "價格: 199.00 TWD"
echo (string)$price; // 明確轉型
6. `__invoke`:可呼叫物件 (Functor)
<?php
class RequestLogger
{
private int $count = 0;
public function __invoke(array $request): void
{
$this->count++;
error_log("Request #{$this->count}: {$request['method']} {$request['uri']}");
}
}
$logger = new RequestLogger();
$logger(['method' => 'GET', 'uri' => '/api/users']); // 直接呼叫物件
$logger(['method' => 'POST', 'uri' => '/api/users']);
// 適用於中介軟體、事件監聽器、策略模式
7. `__clone`:深層複製控制
<?php
class Order
{
public int $id;
public array $items = [];
public \DateTime $createdAt;
public function __clone()
{
// 深層複製 items 陣列 (若為物件需遞迴 clone)
$this->items = array_map(fn($item) => clone $item, $this->items);
// 重新產生建立時間
$this->createdAt = new \DateTime();
// ID 歸零 (新訂單)
$this->id = 0;
}
}
$order1 = new Order();
$order1->id = 1001;
$order1->items[] = new OrderItem('商品A', 2);
$order2 = clone $order1; // 觸發 __clone
// $order2->id = 0, $order2->createdAt = now(), items 為獨立副本
8. `__set_state`:`var_export` 可重建物件
<?php
class Config
{
public array $settings = [];
public static function __set_state(array $properties): self
{
$obj = new self();
$obj->settings = $properties['settings'];
return $obj;
}
}
$config = new Config();
$config->settings = ['debug' => true, 'env' => 'prod'];
$exported = var_export($config, true);
// 輸出: Config::__set_state(array('settings' => array('debug' => true, 'env' => 'prod')))
// 可直接 eval() 還原
9. `__debugInfo`:自訂 `var_dump` 輸出
<?php
class ApiResponse
{
public int $status;
public array $data;
private string $rawBody; // 敏感/過大欄位不想顯示
public function __debugInfo(): array
{
return [
'status' => $this->status,
'data' => $this->data,
'rawBody_length' => strlen($this->rawBody),
];
}
}
$resp = new ApiResponse();
$resp->status = 200;
$resp->data = ['id' => 1];
$resp->rawBody = str_repeat('x', 10000);
var_dump($resp);
// 輸出僅包含 status、data、rawBody_length,不會噴出 10KB 原始內容
最佳實踐與注意事項
- 慎用 `__call`/`__get` 等動態特性:降低 IDE 智慧感應、靜態分析 (PHPStan/Psalm) 準確度、效能 (每次呼叫經過魔術方法分發)。必要時加上 `@method`、`@property` 註解輔助分析。
- 序列化優先用 `__serialize`/`__unserialize`:PHP 7.4+ 標準化、支援型別、避免 `__sleep` 回傳陣列順序問題。
- `__destruct` 中避免複雜邏輯/例外:關閉資源即可,例外將被忽略並產生警告。
- `__clone` 深層複製:包含物件屬性、資源控制代碼、內部 ID 重置。
- `__toString` 必須回傳字串:否則拋出 `Error` (PHP 8.0+),不可拋出例外。
- 魔術方法不可靜態呼叫 (除 `__callStatic`/`__set_state`):`static::method()` 不會觸發 `__call`。
結語
魔術方法是 PHP 物件模型的「擴充點」,讓開發者能在不修改核心語言的前提下,實現動態代理、屬性虛擬化、序列化控制、資源管理、除錯友善等進階模式。熟練掌握 12 個核心魔術方法的觸發條件與契約,配合介面、Trait、屬性 (PHP 8.0+) 使用,能構建極具表達力與彈性的領域模型。但請記住:明確優於隱性,僅在真正需要動態行為時使用,並搭配完整註解與測試確保可維護性。
- 上一篇文章: PHP vs JSP 效能實測對比:算術運算、資料庫存取與並發處理
- 下一篇文章: 從入門到精通:擺脫「偽 PHP 程式設計師」的 7 個關鍵步驟
Copyright ©
程式碼之家 All Rights Reserved