魔術方法總覽
| 方法 | 觸發時機 | 典型用途 |
__get($name) | 讀取不可見/不存在屬性 | 動態屬性代理、資料庫欄位映射 |
__set($name, $value) | 寫入不可見/不存在屬性 | 屬性驗證、自動儲存、關聯模型 |
__call($name, $args) | 呼叫不可見/不存在實例方法 | 動態方法、服務代理、鏈式呼叫 |
__callStatic($name, $args) | 呼叫不可見/不存在靜態方法 | 工廠方法、查詢建構器、門面模式 |
__toString() | 物件當作字串使用 (echo/print/字串串接) | 日誌輸出、API 回應、模板渲染 |
__invoke($args...) | 物件當作函數呼叫 $obj() | 可呼叫物件、策略模式、中介軟體 |
完整實作範例
<?php
class DynamicEntity {
private array $data = [];
private array $relations = [];
// 屬性讀取代理
public function __get(string $name): mixed {
if (array_key_exists($name, $this->data)) {
return $this->data[$name];
}
// 關聯懶載入
if (isset($this->relations[$name])) {
return $this->relations[$name];
}
throw new Exception("Property '$name' not found");
}
// 屬性寫入代理
public function __set(string $name, mixed $value): void {
// 可加入驗證邏輯
if ($name === 'email' && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email');
}
$this->data[$name] = $value;
}
// 動態方法:如 findByEmail、getPosts 等
public function __call(string $name, array $args): mixed {
if (str_starts_with($name, 'findBy')) {
$field = lcfirst(substr($name, 6)); // findByEmail → email
return $this->repository->findBy($field, $args[0] ?? null);
}
if (str_starts_with($name, 'get')) {
$relation = lcfirst(substr($name, 3));
return $this->loadRelation($relation);
}
throw new BadMethodCallException("Method $name not found");
}
// 靜態工廠:User::create(['name'=>'...'])
public static function __callStatic(string $name, array $args): mixed {
if ($name === 'create') {
return (new static())->fill($args[0] ?? [])->save();
}
if ($name === 'find') {
return (new static())->repository->find($args[0] ?? null);
}
throw new BadMethodCallException("Static method $name not found");
}
// 物件轉字串:echo $user → "User#123: John Doe"
public function __toString(): string {
return sprintf("%s#%d: %s", static::class, $this->data['id'] ?? 0, $this->data['name'] ?? 'Unknown');
}
// 物件當函數:$user(['email' => 'new@ex.com']) 更新並儲存
public function __invoke(array $updates): self {
foreach ($updates as $k => $v) {
$this->$k = $v; // 觸發 __set 驗證
}
return $this->save();
}
}
// 使用示範
$user = new DynamicEntity();
$user->name = 'Alice'; // __set
$user->email = 'alice@ex.com'; // __set (驗證)
echo $user->name; // __get → Alice
echo $user; // __toString → DynamicEntity#0: Alice
$user(['name' => 'Alice Updated']); // __invoke
User::create(['name' => 'Bob']); // __callStatic
?>
實戰應用場景
1. ActiveRecord / ORM 基礎類
__get/__set 映射資料表欄位
__call 實現 findBy*、get*Relation 動態查詢
__callStatic 實現 Model::find()、Model::create()
2. 服務門面
<?php
class ServiceFacade {
private array $services = [];
public function __set(string $name, object $service) {
$this->services[$name] = $service;
}
public function __call(string $name, array $args) {
if (isset($this->services[$name])) {
return $this->services[$name]->handle(...$args);
}
throw new Exception("Service $name not registered");
}
}
$facade = new ServiceFacade();
$facade->payment = new PaymentService();
$facade->notification = new NotificationService();
$facade->payment->charge(100); // 實際呼叫 PaymentService::charge
?>
3. 可呼叫策略物件
<?php
interface DiscountStrategy {
public function __invoke(float $price): float;
}
class VipDiscount implements DiscountStrategy {
public function __invoke(float $price): float { return $price * 0.8; }
}
class SeasonalDiscount implements DiscountStrategy {
public function __invoke(float $price): float { return $price * 0.9; }
}
function applyDiscount(float $price, DiscountStrategy $strategy): float {
return $strategy($price); // 直接呼叫物件
}
echo applyDiscount(100, new VipDiscount()); // 80
?>
4. 模板渲染物件
<?php
class View {
private array $vars = [];
public function __set(string $name, mixed $value) { $this->vars[$name] = $value; }
public function __get(string $name) { return $this->vars[$name] ?? null; }
public function __toString() {
extract($this->vars);
ob_start();
include $this->template;
return ob_get_clean();
}
}
$view = new View();
$view->title = '歡迎';
$view->user = ['name' => 'Alice'];
echo $view; // 自動渲染模板
?>
注意事項與效能
- 魔術方法比直接存取慢(需經過方法解析),高頻路徑建議定義實際屬性/方法
__get/__set 遞迴呼叫會導致無限循環,內部存取私有陣列時用 $this->data[$name] 而非 $this->$name
__toString 必須回傳字串,拋出異常會導致 Fatal Error
__invoke 讓物件可當作 callable 傳遞,支援型別提示 callable|Closure|MyInvokable
PHP 8 新增:__serialize / __unserialize
<?php
class DataObject {
private array $data;
public function __serialize(): array { return $this->data; }
public function __unserialize(array $data): void { $this->data = $data; }
}
// 取代舊有 __sleep/__wakeup,支援 typed properties、更安全
?>