PHP 的 __call() 魔術方法在物件導向程式設計中扮演關鍵角色,它允許攔截「不存在」或「不可見」的方法調用,是實作動態代理、委派模式、流暢介面的核心機制。本文從原理、語法到實戰場景完整解析。
<?php
public function __call(string $name, array $arguments): mixed
觸發時機:當呼叫的方法在類別中不存在、或不可見(private/protected 從外部呼叫)時。
<?php
class Service {
public function __call($method, $args) {
echo "嘗試呼叫不存在的方法:{$method}\n";
echo "參數:" . json_encode($args) . "\n";
// 可在這裡實作動態邏輯
return "動態處理 {$method} 的結果";
}
public function realMethod() {
return "真實方法";
}
}
$svc = new Service();
echo $svc->realMethod() . "\n"; // 正常呼叫
echo $svc->dynamicMethod('arg1', 'arg2') . "\n"; // 觸發 __call
輸出:
真實方法
嘗試呼叫不存在的方法:dynamicMethod
參數:["arg1","arg2"]
動態處理 dynamicMethod 的結果
將方法呼叫轉發給內部物件,實現「合成優於繼承」:
<?php
class Logger {
public function log(string $level, string $message) {
echo "[{$level}] {$message}\n";
}
}
class Service {
private $logger;
private $delegates = [];
public function __construct() {
$this->logger = new Logger();
}
public function attach(string $name, object $delegate) {
$this->delegates[$name] = $delegate;
}
public function __call($method, $args) {
// 記錄所有動態呼叫
$this->logger->log('DEBUG', "呼叫:{$method}(" . json_encode($args) . ")");
// 嘗試委派給附加的物件
foreach ($this->delegates as $name => $delegate) {
if (method_exists($delegate, $method)) {
return $delegate->$method(...$args);
}
}
throw new BadMethodCallException("方法 {$method} 不存在且無委派目標");
}
}
// 使用範例
class EmailService {
public function send(string $to, string $subject) {
return "郵件已寄送給 {$to}";
}
}
$service = new Service();
$service->attach('email', new EmailService());
echo $service->send('user@example.com', '歡迎'); // 委派給 EmailService
<?php
class QueryBuilder {
private $conditions = [];
private $orders = [];
private $limit = null;
public function __call($method, $args) {
// 支援 whereName、whereEmail、orderById 等動態方法
if (preg_match('/^where(.+)$/', $method, $matches)) {
$field = lcfirst($matches[1]); // Name -> name
$this->conditions[$field] = $args[0] ?? null;
return $this; // 鏈式呼叫
}
if (preg_match('/^orderBy(.+)$/', $method, $matches)) {
$field = lcfirst($matches[1]);
$this->orders[] = [$field, $args[0] ?? 'ASC'];
return $this;
}
if ($method === 'limit' && count($args) === 1) {
$this->limit = (int)$args[0];
return $this;
}
throw new BadMethodCallException("不支援的方法:{$method}");
}
public function getSql(): string {
$sql = "SELECT * FROM users";
if ($this->conditions) {
$where = [];
foreach ($this->conditions as $field => $value) {
$where[] = "`{$field}` = '" . addslashes($value) . "'";
}
$sql .= " WHERE " . implode(' AND ', $where);
}
if ($this->orders) {
$order = [];
foreach ($this->orders as [$field, $dir]) {
$order[] = "`{$field}` {$dir}";
}
$sql .= " ORDER BY " . implode(', ', $order);
}
if ($this->limit) {
$sql .= " LIMIT {$this->limit}";
}
return $sql;
}
}
$qb = new QueryBuilder();
$sql = $qb->whereName('John')
->whereEmail('john@example.com')
->orderByCreatedAt('DESC')
->limit(10)
->getSql();
echo $sql;
// SELECT * FROM users WHERE `name` = 'John' AND `email` = 'john@example.com' ORDER BY `created_at` DESC LIMIT 10
| 項目 | 說明 |
|---|---|
| 效能影響 | 每次不存在方法呼叫都會觸發,高頻熱路徑建議改用陣列映射或實際定義方法 |
| IDE 支援 | 動態方法無法被靜態分析,建議搭配 @method 註解 |
| 遞迴風險 | 在 __call 內呼叫 $this->sameMethod() 會無限遞迴,需謹慎處理 |
| 靜態呼叫 | 靜態呼叫不存在方法請用 __callStatic() |
| 繼承行為 | 子類別未覆寫 __call 時會繼承父類別實作 |
BadMethodCallException@method PHPDoc 標籤讓 IDE 提供提示__call() 是 PHP 物件模型的強大擴展,善用它能實現優雅的動態行為,但濫用會導致程式碼難以維護與除錯。理解其機制與邊界,才能在適當場合發揮最大效用。