PHP 的魔術方法賦予類別「元編程」能力,其中 __call() 與 __callStatic() 是實現動態方法呼叫、方法代理、流暢介面、ORM 欄位存取器 的核心機制。本文將從原理、參數、生命週期、實戰模式、效能考量全方位解析。
$obj->methodName(...)public function __call(string $name, array $arguments): mixed
$name:被呼叫的方法名稱(字串)$arguments:傳遞給該方法的參數陣列(數值索引)<?php
class DynamicObject {
private array $data = [];
public function __call(string $name, array $args): mixed {
// 解析 setXxx / getXxx 模式
if (preg_match('/^set([A-Z].*)$/', $name, $matches)) {
$property = lcfirst($matches[1]); // 駝峰轉小駝峰
$this->data[$property] = $args[0] ?? null;
return $this; // 支援鏈式呼叫
}
if (preg_match('/^get([A-Z].*)$/', $name, $matches)) {
$property = lcfirst($matches[1]);
return $this->data[$property] ?? null;
}
throw new BadMethodCallException("Method $name does not exist");
}
}
$obj = new DynamicObject();
$obj->setName('Alice')->setAge(25); // 鏈式呼叫
echo $obj->getName(); // Alice
echo $obj->getAge(); // 25
?>
<?php
class StaticFactory {
public static function __callStatic(string $name, array $args): mixed {
if (str_starts_with($name, 'create')) {
$class = 'App\\Models\\' . substr($name, 6); // createUser -> User
if (class_exists($class)) {
return new $class(...$args);
}
}
throw new BadMethodCallException("Static method $name not found");
}
}
// 使用
$user = StaticFactory::createUser('Alice', 'alice@example.com');
$post = StaticFactory::createPost('Title', 'Content');
?>
<?php
class Model {
protected array $attributes = [];
protected array $casts = [];
public function __call(string $method, array $args) {
// setAttribute / getAttribute
if ($method === 'setAttribute') {
[$key, $value] = $args;
$this->attributes[$key] = $this->castValue($key, $value);
return $this;
}
if ($method === 'getAttribute') {
return $this->attributes[$args[0]] ?? null;
}
// 動態 getXxx / setXxx
if (preg_match('/^(get|set)([A-Z].+)$/', $method, $m)) {
$action = strtolower($m[1]);
$attr = $this->camelToSnake($m[2]);
if ($action === 'get') {
return $this->getAttribute($attr);
}
$this->setAttribute($attr, $args[0]);
return $this;
}
throw new BadMethodCallException("Method $method not found");
}
private function camelToSnake(string $str): string {
return strtolower(preg_replace('/([A-Z])/', '_$1', $str));
}
private function castValue(string $key, mixed $value): mixed {
return match ($this->casts[$key] ?? null) {
'int' => (int)$value,
'float' => (float)$value,
'bool' => (bool)$value,
'array', 'json' => json_decode($value, true),
'datetime' => new DateTime($value),
default => $value,
};
}
}
// 使用
class User extends Model {
protected array $casts = [
'is_active' => 'bool',
'settings' => 'json',
'last_login' => 'datetime',
];
}
$user = new User();
$user->setName('Alice')->setIsActive(true)->setSettings(['theme' => 'dark']);
echo $user->getName(); // Alice
echo $user->getIsActive(); // true (bool)
print_r($user->getSettings()); // ['theme' => 'dark']
?>
<?php
class QueryBuilder {
private array $parts = ['select' => '*', 'where' => [], 'order' => []];
public function __call(string $method, array $args): self {
$part = strtolower($method);
if (isset($this->parts[$part])) {
$this->parts[$part] = array_merge(
(array)$this->parts[$part],
$args
);
return $this;
}
throw new BadMethodCallException("Unknown query part: $method");
}
public function getSql(): string {
$sql = "SELECT {$this->parts['select']} FROM table";
if ($this->parts['where']) {
$sql .= ' WHERE ' . implode(' AND ', $this->parts['where']);
}
if ($this->parts['order']) {
$sql .= ' ORDER BY ' . implode(', ', $this->parts['order']);
}
return $sql;
}
}
$sql = (new QueryBuilder())
->select('id', 'name', 'email')
->where('status = ?', 'active')
->where('created_at > ?', '2024-01-01')
->order('name ASC')
->getSql();
// SELECT id, name, email FROM table WHERE status = ? AND created_at > ? ORDER BY name ASC
?>
<?php
class ServiceProxy {
public function __construct(private object $service) {}
public function __call(string $method, array $args): mixed {
// 記錄呼叫日誌
error_log("Calling $method with " . json_encode($args));
// 計時
$start = microtime(true);
try {
$result = $this->service->$method(...$args);
return $result;
} finally {
$duration = microtime(true) - $start;
error_log("$method took " . round($duration * 1000, 2) . "ms");
}
}
}
// 使用:透明代理任何服務
$proxy = new ServiceProxy(new RealPaymentService());
$proxy->charge(100, 'USD'); // 自動記錄日誌與計時
?>
PHP 方法解析順序:
__call()(當以上皆找不到時)__call()<?php
class ParentClass {
public function __call($name, $args) {
echo "Parent::__call($name)\n";
}
}
class ChildClass extends ParentClass {
public function existingMethod() {
echo "Real method\n";
}
public function __call($name, $args) {
echo "Child::__call($name)\n";
}
}
$obj = new ChildClass();
$obj->existingMethod(); // Real method(真實方法優先)
$obj->missingMethod(); // Child::__call(missingMethod)(子類別 __call 優先)
?>
__call() 涉及字串解析、正則比對、反射式調用,比直接呼叫慢 10-50 倍<?php
class OptimizedModel {
private array $attributes = [];
private array $accessorCache = []; // 快取解析結果
public function __call(string $name, array $args) {
// 快取方法解析結果
if (!isset($this->accessorCache[$name])) {
$this->accessorCache[$name] = $this->resolveAccessor($name);
}
$accessor = $this->accessorCache[$name];
if ($accessor === null) {
throw new BadMethodCallException("Method $name not found");
}
return $accessor($args);
}
private function resolveAccessor(string $name): ?callable {
// 編譯期解析,執行期直接呼叫閉包
if (preg_match('/^get([A-Z].+)$/', $name, $m)) {
$attr = $this->camelToSnake($m[1]);
return fn() => $this->attributes[$attr] ?? null;
}
if (preg_match('/^set([A-Z].+)$/', $name, $m)) {
$attr = $this->camelToSnake($m[1]);
return fn($args) => $this->setAttribute($attr, $args[0]);
}
return null;
}
}
?>
<?php
/**
* @method static User createUser(string $name, string $email)
* @method static Post createPost(string $title, string $content)
* @method string getName()
* @method $this setName(string $name)
* @method int getAge()
* @method $this setAge(int $age)
*/
class DynamicModel { /* ... */ }
?>
<?php
class DebuggableModel {
public function __call(string $name, array $args) {
// ...
}
public function getDynamicMethods(): array {
$methods = [];
foreach ($this->attributes as $attr => $value) {
$camel = $this->snakeToCamel($attr);
$methods[] = "get$camel";
$methods[] = "set$camel";
}
return $methods;
}
}
?>
| 錯誤 | 原因 | 修正 |
|---|---|---|
| 遞迴無限循環 | __call 內呼叫不存在的方法 | 檢查方法存在性、使用 isset($this->data[$key]) |
| 靜態呼叫觸發 __call | 靜態語境應用 __callStatic | 實作 __callStatic 或改用實例呼叫 |
| 參數陣列索引錯誤 | $arguments 為 0 基索引數值陣列 | 使用 ...$arguments 解包或 $arguments[0] |
| 型別提示缺失 | 動態回傳型別無法靜態分析 | 加上 @method 註解、回傳 mixed、PHPStan 忽略 |
| 效能瓶頸 | 熱路徑使用 __call | 快取解析結果、改用固定方法、代碼生成 |
__call() 是 PHP 動態特性的集中體現,支撐了 Laravel Eloquent、Doctrine ORM、各類建構器與代理模式的核心實作。理解其觸發機制、參數結構、優先順序與效能特徵,能讓你在需要靈活性時大膽使用,在追求效能時果斷優化。記住核心原則:動態能力要有邊界,明確契約優於隱性魔法。