物件導向封裝原則建議將屬性設為 private,但頻繁的存取器 (getter/setter) 撰寫繁瑣。PHP 5 提供 __set() 與 __get() 魔術方法,讓我們能在維持封裝性的同時,享受如同公開屬性般直觀的存取語法。本文深入解析攔截器機制與進階應用。
__set($name, $value):寫入不可存取屬性時 (private、protected 或不存在)__get($name):讀取不可存取屬性時__isset($name):對不可存取屬性呼叫 isset() 或 empty()__unset($name):對不可存取屬性呼叫 unset()class Computer {
private $data = []; // 內部儲存容器
// 寫入攔截
public function __set($name, $value) {
echo "設定屬性: {$name} = {$value}\n";
$this->data[$name] = $value;
}
// 讀取攔截
public function __get($name) {
echo "讀取屬性: {$name}\n";
return $this->data[$name] ?? null;
}
// isset/empty 支援
public function __isset($name) {
return isset($this->data[$name]);
}
// unset 支援
public function __unset($name) {
unset($this->data[$name]);
}
}
// 使用方式如同公開屬性
$computer = new Computer();
$computer->brand = 'Dell'; // 觸發 __set
$computer->price = 30000;
echo $computer->brand; // 觸發 __get,輸出 Dell
echo isset($computer->price); // 觸發 __isset,輸出 true
unset($computer->price); // 觸發 __unset
echo isset($computer->price); // false
class Product {
private $data = [];
private $rules = [
'price' => ['type' => 'float', 'min' => 0],
'stock' => ['type' => 'int', 'min' => 0],
'name' => ['type' => 'string', 'max' => 100],
'tags' => ['type' => 'array'],
'active' => ['type' => 'bool'],
];
public function __set($name, $value) {
if (!isset($this->rules[$name])) {
throw new Exception("未定義的屬性: {$name}");
}
$rule = $this->rules[$name];
// 型別轉換與驗證
switch ($rule['type']) {
case 'int':
$value = (int) $value;
break;
case 'float':
$value = (float) $value;
break;
case 'bool':
$value = (bool) $value;
break;
case 'array':
if (!is_array($value)) {
throw new Exception("{$name} 必須為陣列");
}
break;
case 'string':
$value = (string) $value;
if (isset($rule['max']) && strlen($value) > $rule['max']) {
throw new Exception("{$name} 長度不可超過 {$rule['max']}");
}
break;
}
// 範圍驗證
if (isset($rule['min']) && $value < $rule['min']) {
throw new Exception("{$name} 不可小於 {$rule['min']}");
}
$this->data[$name] = $value;
}
public function __get($name) {
return $this->data[$name] ?? null;
}
}
$product = new Product();
$product->price = '299.99'; // 字串自動轉 float
$product->stock = '100'; // 字串自動轉 int
$product->active = 'true'; // 字串自動轉 bool
$product->tags = ['hot', 'sale'];
// 驗證生效
// $product->price = -10; // Exception: price 不可小於 0
// $product->name = str_repeat('a', 101); // Exception: 長度超過
class Order {
private $items = [];
private $createdAt;
public function __construct() {
$this->createdAt = new DateTime();
}
public function addItem($product, $quantity, $price) {
$this->items[] = compact('product', 'quantity', 'price');
}
// 唯讀屬性:只能在建構子設定
public function __set($name, $value) {
if ($name === 'createdAt' && !$this->createdAt) {
$this->createdAt = $value instanceof DateTime ? $value : new DateTime($value);
} else {
throw new Exception("屬性 {$name} 為唯讀");
}
}
// 計算屬性:動態計算總價
public function __get($name) {
switch ($name) {
case 'totalAmount':
return array_reduce($this->items, fn($sum, $item) =>
$sum + $item['quantity'] * $item['price'], 0);
case 'itemCount':
return count($this->items);
case 'createdAt':
return $this->createdAt;
default:
return null;
}
}
public function __isset($name) {
return in_array($name, ['totalAmount', 'itemCount', 'createdAt']);
}
}
$order = new Order();
$order->addItem('滑鼠', 2, 500);
$order->addItem('鍵盤', 1, 1500);
echo $order->totalAmount; // 2500 (動態計算)
echo $order->itemCount; // 2
echo $order->createdAt->format('Y-m-d H:i:s'); // 建立時間
結合 ArrayAccess 介面,讓物件支援陣列語法:
class Config implements ArrayAccess {
private $data = [];
// 物件語法
public function __set($name, $value) { $this->data[$name] = $value; }
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]); }
// 陣列語法
public function offsetSet($offset, $value) { $this->data[$offset] = $value; }
public function offsetGet($offset) { return $this->data[$offset] ?? null; }
public function offsetExists($offset) { return isset($this->data[$offset]); }
public function offsetUnset($offset) { unset($this->data[$offset]); }
}
$config = new Config();
$config->db_host = 'localhost'; // 物件語法
$config['db_port'] = 3306; // 陣列語法
echo $config->db_host; // localhost
echo $config['db_port']; // 3306
| 做法 | 建議 |
|---|---|
| 文件化動態屬性 | 使用 @property 註解讓 IDE 支援自動完成 |
| 嚴格驗證屬性名 | 在 __set 中維護白名單,拒絕任意屬性寫入 |
| 實作 __isset/__unset | 確保 isset()/empty()/unset() 行為一致 |
| 避免副作用 | __get 應為純函式,不修改狀態 |
| 搭配 __debugInfo | 控制 var_dump 輸出,避免洩露敏感資料 |