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

PHP 5 魔術方法完整指南:__construct、__call、__get 等 12 個方法實戰應用

     編輯:PHP 相關知識

前言

PHP 5 引入魔術方法,以雙底線 `__` 開頭,在特定生命週期或語言事件觸發時自動被呼叫。它們賦予物件動態行為能力,是實現延遲載入、屬性重載、序列化控制、呼叫攔截等進階模式的關鍵。本文詳細解說 12 個核心魔術方法的觸發時機、參數、回傳值與實戰範例。

魔術方法總覽

注意:`__sleep`/`__wakeup` 為舊版序列化介面,PHP 7.4+ 建議使用 `__serialize`/`__unserialize` (支援型別、避免兼容性問題)。

詳細範例與應用場景

1. 建構子與解構子:資源管理

<?php
class DatabaseConnection
{
    private PDO $pdo;
    private string $dsn;

    public function __construct(string $dsn, string $user, string $pass)
    {
        $this->dsn = $dsn;
        $this->pdo = new PDO($dsn, $user, $pass, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
        echo "[DB] 連線建立: $dsn\n";
    }

    public function query(string $sql, array $params = []): array
    {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll();
    }

    public function __destruct()
    {
        // PDO 會自動關閉,這裡示範額外清理邏輯
        $this->pdo = null;
        echo "[DB] 連線關閉: {$this->dsn}\n";
    }
}

// 使用
$db = new DatabaseConnection('mysql:host=localhost;dbname=test', 'root', '');
$users = $db->query('SELECT * FROM users LIMIT 5');
// 腳本結束或 $db 超出作用域時自動呼叫 __destruct

2. `__call` / `__callStatic`:動態方法與代理

<?php
class ApiClient
{
    private string $baseUrl;
    private array $endpoints = ['users', 'posts', 'comments'];

    public function __construct(string $baseUrl) { $this->baseUrl = $baseUrl; }

    // 動態建立 $client->users()->get() 等鏈式呼叫
    public function __call(string $name, array $arguments)
    {
        if (in_array($name, $this->endpoints)) {
            return new EndpointProxy($this->baseUrl . "/$name");
        }
        throw new BadMethodCallException("Endpoint '$name' 不存在");
    }

    // 靜態快捷呼叫:ApiClient::users()->get()
    public static function __callStatic(string $name, array $arguments)
    {
        if (in_array($name, (new self(''))->endpoints)) {
            return (new self('https://api.example.com'))->$name(...$arguments);
        }
        throw new BadMethodCallException("Static endpoint '$name' 不存在");
    }
}

class EndpointProxy
{
    private string $url;
    public function __construct(string $url) { $this->url = $url; }

    public function get(array $query = []): array
    {
        $url = $this->url . '?' . http_build_query($query);
        return json_decode(file_get_contents($url), true) ?? [];
    }
    public function post(array $data): array { /* ... */ }
}

// 使用
$client = new ApiClient('https://jsonplaceholder.typicode.com');
$users = $client->users()->get(['limit' => 5]); // 觸發 __call('users') -> EndpointProxy->get()
$posts = ApiClient::posts()->get(); // 觸發 __callStatic('posts')

3. `__get` / `__set` / `__isset` / `__unset`:屬性重載與虛擬屬性

<?php
class UserModel
{
    private array $data = [];
    private array $dirty = []; // 追蹤異動欄位

    // 只允許存取白名單欄位
    private array $fillable = ['name', 'email', 'password', 'avatar'];

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

    public function __set(string $name, mixed $value): void
    {
        if (!in_array($name, $this->fillable)) {
            throw new Exception("欄位 '$name' 不允許寫入");
        }
        $this->data[$name] = $value;
        $this->dirty[$name] = true;
    }

    public function __isset(string $name): bool
    {
        return isset($this->data[$name]);
    }

    public function __unset(string $name): void
    {
        unset($this->data[$name], $this->dirty[$name]);
    }

    public function getDirty(): array { return array_keys($this->dirty); }
    public function toArray(): array { return $this->data; }
}

$user = new UserModel();
$user->name = 'John'; // 觸發 __set
$user->email = 'john@example.com';
echo $user->name; // 觸發 __get
isset($user->name); // true, 觸發 __isset
unset($user->name); // 觸發 __unset
echo $user->getDirty(); // ['name', 'email']

4. `__sleep` / `__wakeup` / `__serialize` / `__unserialize`:序列化控制

<?php
class SessionData
{
    public string $userId;
    public array $permissions = [];
    private PDO $db; // 資源不可序列化
    private string $cacheKey;

    // PHP 7.4+ 建議使用 __serialize/__unserialize
    public function __serialize(): array
    {
        return [
            'userId' => $this->userId,
            'permissions' => $this->permissions,
            'cacheKey' => $this->cacheKey,
        ];
    }

    public function __unserialize(array $data): void
    {
        $this->userId = $data['userId'];
        $this->permissions = $data['permissions'];
        $this->cacheKey = $data['cacheKey'];
        // 反序列化後重新建立資源連線
        $this->db = new PDO('mysql:host=localhost;dbname=test', 'root', '');
    }

    // 舊版相容 (PHP < 7.4)
    public function __sleep(): array
    {
        return ['userId', 'permissions', 'cacheKey']; // 排除 $db
    }

    public function __wakeup(): void
    {
        $this->db = new PDO('mysql:host=localhost;dbname=test', 'root', '');
    }
}

5. `__toString`:物件字串化

<?php
class Money
{
    public function __construct(
        public int $amount, // 分
        public string $currency = 'TWD'
    ) {}

    public function __toString(): string
    {
        return sprintf('%.2f %s', $this->amount / 100, $this->currency);
    }
}

$price = new Money(19900, 'TWD');
echo "價格: $price"; // 自動呼叫 __toString -> "價格: 199.00 TWD"
echo (string)$price; // 明確轉型

6. `__invoke`:可呼叫物件 (Functor)

<?php
class RequestLogger
{
    private int $count = 0;

    public function __invoke(array $request): void
    {
        $this->count++;
        error_log("Request #{$this->count}: {$request['method']} {$request['uri']}");
    }
}

$logger = new RequestLogger();
$logger(['method' => 'GET', 'uri' => '/api/users']); // 直接呼叫物件
$logger(['method' => 'POST', 'uri' => '/api/users']);
// 適用於中介軟體、事件監聽器、策略模式

7. `__clone`:深層複製控制

<?php
class Order
{
    public int $id;
    public array $items = [];
    public \DateTime $createdAt;

    public function __clone()
    {
        // 深層複製 items 陣列 (若為物件需遞迴 clone)
        $this->items = array_map(fn($item) => clone $item, $this->items);
        // 重新產生建立時間
        $this->createdAt = new \DateTime();
        // ID 歸零 (新訂單)
        $this->id = 0;
    }
}

$order1 = new Order();
$order1->id = 1001;
$order1->items[] = new OrderItem('商品A', 2);
$order2 = clone $order1; // 觸發 __clone
// $order2->id = 0, $order2->createdAt = now(), items 為獨立副本

8. `__set_state`:`var_export` 可重建物件

<?php
class Config
{
    public array $settings = [];

    public static function __set_state(array $properties): self
    {
        $obj = new self();
        $obj->settings = $properties['settings'];
        return $obj;
    }
}

$config = new Config();
$config->settings = ['debug' => true, 'env' => 'prod'];
$exported = var_export($config, true);
// 輸出: Config::__set_state(array('settings' => array('debug' => true, 'env' => 'prod')))
// 可直接 eval() 還原

9. `__debugInfo`:自訂 `var_dump` 輸出

<?php
class ApiResponse
{
    public int $status;
    public array $data;
    private string $rawBody; // 敏感/過大欄位不想顯示

    public function __debugInfo(): array
    {
        return [
            'status' => $this->status,
            'data' => $this->data,
            'rawBody_length' => strlen($this->rawBody),
        ];
    }
}

$resp = new ApiResponse();
$resp->status = 200;
$resp->data = ['id' => 1];
$resp->rawBody = str_repeat('x', 10000);
var_dump($resp);
// 輸出僅包含 status、data、rawBody_length,不會噴出 10KB 原始內容

最佳實踐與注意事項

  1. 慎用 `__call`/`__get` 等動態特性:降低 IDE 智慧感應、靜態分析 (PHPStan/Psalm) 準確度、效能 (每次呼叫經過魔術方法分發)。必要時加上 `@method`、`@property` 註解輔助分析。
  2. 序列化優先用 `__serialize`/`__unserialize`:PHP 7.4+ 標準化、支援型別、避免 `__sleep` 回傳陣列順序問題。
  3. `__destruct` 中避免複雜邏輯/例外:關閉資源即可,例外將被忽略並產生警告。
  4. `__clone` 深層複製:包含物件屬性、資源控制代碼、內部 ID 重置。
  5. `__toString` 必須回傳字串:否則拋出 `Error` (PHP 8.0+),不可拋出例外。
  6. 魔術方法不可靜態呼叫 (除 `__callStatic`/`__set_state`):`static::method()` 不會觸發 `__call`。

結語

魔術方法是 PHP 物件模型的「擴充點」,讓開發者能在不修改核心語言的前提下,實現動態代理、屬性虛擬化、序列化控制、資源管理、除錯友善等進階模式。熟練掌握 12 個核心魔術方法的觸發條件與契約,配合介面、Trait、屬性 (PHP 8.0+) 使用,能構建極具表達力與彈性的領域模型。但請記住:明確優於隱性,僅在真正需要動態行為時使用,並搭配完整註解與測試確保可維護性。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
方法觸發時機參數回傳值要求
__construct()物件實例化時 (`new Class()`)任意無 (void)
__destruct()物件銷毀時 (引用計數歸零、腳本結束)無無
__call()呼叫不可存取/不存在的實例方法`string $name, array $arguments`任意 (呼叫者將收到)
__callStatic()呼叫不可存取/不存在的靜態方法`string $name, array $arguments`任意
__get()讀取不可存取/不存在的屬性`string $name`任意
__set()寫入不可存取/不存在的屬性`string $name, mixed $value`無
__isset()對不可存取屬性呼叫 `isset()`/`empty()``string $name``bool`
__unset()對不可存取屬性呼叫 `unset()``string $name`無
__sleep()`serialize($obj)` 前無`array` (需序列化的屬性名稱陣列)
__wakeup()`unserialize($data)` 後無無
__serialize()`serialize($obj)` (PHP 7.4+ 優先)無`array` (鍵值對陣列)
__unserialize()`unserialize($data)` (PHP 7.4+ 優先)`array $data`無
__toString()物件被當作字串使用 (`echo $obj`, `(string)$obj`)無`string` (否則拋出 Error)
__invoke()物件被當作函數呼叫 (`$obj()`)任意任意
__set_state()`var_export($obj)` 時 (static)`array $properties`物件實例
__clone()`clone $obj` 後無無
__debugInfo()`var_dump($obj)` 時 (PHP 5.6+)無`array` (顯示的屬性鍵值對)