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

PHP 魔術方法 __get() 與 __set() 實現屬性存取控制與動態屬性

     編輯:PHP 相關知識

PHP __get() / __set() 魔術方法:屬性存取攔截與動態屬性實作

PHP 提供 __get()、__set()、__isset()、__unset() 四個魔術方法,攔截對不可見屬性(未定義、protected/private、父類別無權存取)的讀寫操作。這是實作動態屬性、資料映射、唯讀屬性、延遲載入的關鍵機制。

基本語法與觸發條件

<?php
class Entity {
    private array $data = [];
    private array $allowed = ['name', 'email', 'age'];

    // 讀取不可見屬性時觸發
    public function __get(string $name): mixed {
        if (array_key_exists($name, $this->data)) {
            return $this->data[$name];
        }
        // 可選:拋出例外、回傳 null、觸發錯誤
        throw new Exception("屬性 '$name' 不存在");
    }

    // 寫入不可見屬性時觸發
    public function __set(string $name, mixed $value): void {
        if (in_array($name, $this->allowed, true)) {
            $this->data[$name] = $value;
            return;
        }
        throw new Exception("不允許設定屬性 '$name'");
    }

    // isset()/empty() 檢查不可見屬性時觸發
    public function __isset(string $name): bool {
        return array_key_exists($name, $this->data);
    }

    // unset() 不可見屬性時觸發
    public function __unset(string $name): void {
        unset($this->data[$name]);
    }
}

$user = new Entity();
$user->name = 'John';      // 呼叫 __set('name', 'John')
echo $user->name;          // 呼叫 __get('name') -> 'John'
isset($user->email);       // 呼叫 __isset('email') -> false
unset($user->name);        // 呼叫 __unset('name')
?>

核心應用場景

1. 動態屬性 / 資料映射

class Model {
    protected array $attributes = [];
    protected array $casts = []; // 型別轉換

    public function __get(string $key): mixed {
        $value = $this->attributes[$key] ?? null;
        return $this->castAttribute($key, $value);
    }

    public function __set(string $key, mixed $value): void {
        $this->attributes[$key] = $value;
    }

    protected function castAttribute(string $key, mixed $value): mixed {
        return match ($this->casts[$key] ?? null) {
            'int' => (int)$value,
            'float' => (float)$value,
            'bool' => (bool)$value,
            'array' => json_decode($value, true),
            'json' => json_decode($value),
            'datetime' => new DateTime($value),
            default => $value,
        };
    }
}

class User extends Model {
    protected array $casts = [
        'id' => 'int',
        'is_active' => 'bool',
        'settings' => 'json',
        'last_login' => 'datetime',
    ];
}

$user = new User();
$user->settings = ['theme' => 'dark', 'lang' => 'zh']; // 自動 JSON 編碼儲存
echo $user->settings['theme']; // 自動 JSON 解碼 -> 'dark'

2. 唯讀屬性

class Config {
    private array $values = [];

    public function __set(string $name, mixed $value): void {
        if (array_key_exists($name, $this->values)) {
            throw new Exception("唯讀屬性 '$name' 不可修改");
        }
        $this->values[$name] = $value;
    }

    public function __get(string $name): mixed {
        return $this->values[$name] ?? null;
    }
}

$config = new Config();
$config->app_name = 'MyApp'; // 成功
$config->app_name = 'Other'; // Exception: 唯讀屬性

3. 延遲載入 / 關聯模型

class Post {
    private ?User $author = null;
    private int $authorId;

    public function __construct(int $authorId) {
        $this->authorId = $authorId;
    }

    public function __get(string $name): mixed {
        if ($name === 'author') {
            if ($this->author === null) {
                $this->author = User::find($this->authorId); // 首次存取才查詢
            }
            return $this->author;
        }
        throw new Exception("屬性 '$name' 不存在");
    }
}

$post = new Post(123);
echo $post->author->name; // 觸發查詢,後續存取使用快取

4. 陣列式存取 (ArrayAccess) 結合

class Collection implements ArrayAccess {
    private array $items = [];

    public function offsetExists(mixed $offset): bool {
        return isset($this->items[$offset]);
    }

    public function offsetGet(mixed $offset): mixed {
        return $this->items[$offset] ?? null;
    }

    public function offsetSet(mixed $offset, mixed $value): void {
        $this->items[$offset] = $value;
    }

    public function offsetUnset(mixed $offset): void {
        unset($this->items[$offset]);
    }

    // 同時支援物件語法
    public function __get(string $name): mixed {
        return $this->offsetGet($name);
    }

    public function __set(string $name, mixed $value): void {
        $this->offsetSet($name, $value);
    }
}

效能考量與最佳實踐

面向建議
效能魔術方法比直接屬性存取慢 ~2-5x,熱點路徑避免使用,或加快取快取
型別提示PHP 8.2+ 可用 #[Property] 標註動態屬性,IDE 支援較好
除錯var_dump() 不顯示動態屬性,實作 __debugInfo() 回傳可見屬性陣列
序列化實作 __sleep()/__wakeup() 或 Serializable 控制序列化行為
繼承子類別重寫魔術方法時記得 parent::__get() 處理父類別屬性
唯讀屬性 (PHP 8.1+)簡單唯讀優先使用 readonly 關鍵字,無需魔術方法

PHP 8.2+ 動態屬性棄用警告

PHP 8.2 起,對未宣告屬性的動態建立發出 Deprecated 警告。解決方案:

  1. 在類別宣告所有可能屬性(含 #[AllowDynamicProperties] 屬性)。
  2. 使用 #[AllowDynamicProperties] 標記類別(明確表示設計為動態)。
  3. 改用內部陣列 $this->data[$name] 而非直接動態屬性。
#[AllowDynamicProperties]
class DynamicEntity {
    // 明確允許動態屬性
}

class ModernEntity {
    // 明確宣告所有屬性
    public string $name = '';
    private array $extra = [];
    
    public function __get(string $name): mixed {
        return $this->extra[$name] ?? null;
    }
}

常見錯誤避坑

  • 遞迴呼叫:__set 內部 $this->name = $value 會再次觸發 __set → 無限遞迴。必須操作內部陣列 $this->data[$name] = $value。
  • 未實作 __isset:導致 isset($obj->prop) 永遠回傳 false,empty() 行為異常。
  • 公開內部陣列:return $this->data 會暴露內部結構,應回傳副本或實作 Iterator。
  • 型別不一致:__get 回傳型別與 PHPDoc 不符,靜態分析報錯。

__get/__set 是 PHP 物件導向的進階杠桿,善用可實作優雅的 ORM、設定管理、資料轉換層;濫用則導致程式碼難以追蹤、效能下降、靜態分析失效。核心原則:內部儲存用陣列,對外暴露受控介面,明確處理不存在屬性的情況。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved