首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 攔截器 __set() 與 __get() 完整指南:封裝私有屬性的優雅實作

     編輯:PHP 綜合知識

前言

物件導向封裝原則建議將屬性設為 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 介面)

結合 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

五、效能考量與最佳實務

效能影響

  • 魔術方法比直接屬性存取慢約 2-3 倍 (因需經過方法呼叫機制)
  • 在高頻存取路徑 (如迴圈中) 應避免使用,改用公開屬性或存取器方法

最佳實務清單

IDE 支援註解範例

/**
 * @property string $brand 電腦品牌
 * @property float $price 價格
 * @property int $stock 庫存
 * @property-read float $totalValue 總價值 (計算屬性)
 */
class Computer {
    // ...
}

結語

__set() 與 __get() 是 PHP 實現動態屬性存取的核心機制,讓封裝與便利性得以兼得。關鍵在於建立清晰的屬性規範 (白名單、型別、驗證),並實作完整的四個攔截器確保行為一致。適度使用能大幅減少樣板程式碼,但效能敏感路徑仍建議使用傳統存取器。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
做法建議
文件化動態屬性使用 @property 註解讓 IDE 支援自動完成
嚴格驗證屬性名在 __set 中維護白名單,拒絕任意屬性寫入
實作 __isset/__unset確保 isset()/empty()/unset() 行為一致
避免副作用__get 應為純函式,不修改狀態
搭配 __debugInfo控制 var_dump 輸出,避免洩露敏感資料