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')
?>
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'
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: 唯讀屬性
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; // 觸發查詢,後續存取使用快取
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 起,對未宣告屬性的動態建立發出 Deprecated 警告。解決方案:
#[AllowDynamicProperties] 屬性)。#[AllowDynamicProperties] 標記類別(明確表示設計為動態)。$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($obj->prop) 永遠回傳 false,empty() 行為異常。return $this->data 會暴露內部結構,應回傳副本或實作 Iterator。__get 回傳型別與 PHPDoc 不符,靜態分析報錯。__get/__set 是 PHP 物件導向的進階杠桿,善用可實作優雅的 ORM、設定管理、資料轉換層;濫用則導致程式碼難以追蹤、效能下降、靜態分析失效。核心原則:內部儲存用陣列,對外暴露受控介面,明確處理不存在屬性的情況。