歡迎來到《21天學會PHP 8.4》的第十三天!在前兩天,我們學習了繼承(垂直複用)和接口(行為契約)。但 PHP 是單繼承語言——一個類只能有一個父類。那如果多個不相關的類需要共享同一段功能代碼(比如日誌、緩存、驗證),該怎麼辦?

答案就是今天要學的 PHP 獨有特性:Trait(特質)。Trait 提供了一種水平復用(Horizontal Reuse) 的機制,讓你像“拼積木”一樣組合功能,突破單繼承的限制。

今日目標

  1. 理解 Trait 的概念與使用場景
  2. 學會定義和使用 Trait(trait / use
  3. 掌握多個 Trait 的組合與衝突解決(insteadof / as
  4. 瞭解 Trait 與繼承、接口的關係
  5. 避免 Trait 的常見陷阱

一、為什麼需要 Trait?

問題:單繼承的侷限

class User extends Model { /* ... */ }
class Product extends Model { /* ... */ }

// 現在 User 和 Product 都需要“可序列化為 JSON”
// 但它們已經繼承了 Model,無法再繼承 JsonSerializableBase

解決方案:Trait

trait JsonSerializableTrait {
    public function toJson(): string {
        return json_encode($this, JSON_UNESCAPED_UNICODE);
    }
}

class User extends Model {
    use JsonSerializableTrait;
}

class Product extends Model {
    use JsonSerializableTrait;
}

優勢

  • 多個不相關類共享代碼
  • 不破壞繼承鏈
  • 比組合(Composition)更簡潔(無需額外對象)

二、定義與使用 Trait

基本語法

// 定義 Trait
trait SayHello {
    public function hello(): void {
        echo "Hello from " . static::class . "\n";
    }
}

// 在類中使用
class Person {
    use SayHello;
}

$p = new Person();
$p->hello(); // Hello from Person

🔑 Trait 中可以使用 $this,就像它屬於當前類一樣!

Trait 可包含:

  • 屬性(包括私有屬性)
  • 方法(public/protected/private)
  • 抽象方法(強制使用類實現)
  • 靜態成員

三、多個 Trait 與衝突解決

當多個 Trait 定義了同名方法,PHP 會報錯,必須手動解決衝突

示例:衝突場景

trait Loggable {
    public function log(string $msg): void {
        echo "[LOG] $msg\n";
    }
}

trait Auditable {
    public function log(string $msg): void {
        echo "[AUDIT] $msg\n";
    }
}

class Order {
    use Loggable, Auditable; // ❌ Fatal error: Trait method log has not been applied
}

解決方案 1:insteadof —— 選擇保留哪個

class Order {
    use Loggable, Auditable {
        Loggable::log insteadof Auditable; // 使用 Loggable 的 log
    }
}

解決方案 2:as —— 重命名方法

class Order {
    use Loggable, Auditable {
        Auditable::log as auditLog; // 將 Auditable 的 log 重命名為 auditLog
    }
}

$order = new Order();
$order->log("普通日誌");      // [LOG] 普通日誌
$order->auditLog("審計日誌"); // [AUDIT] 審計日誌

最佳實踐:儘量避免同名方法;若不可避免,用 as 提供清晰別名。


四、Trait 與抽象方法、屬性

Trait 中定義抽象方法

強制使用 Trait 的類必須實現該方法:

trait Timestampable {
    public function getCreatedAt(): string {
        return $this->formatDate($this->createdAt);
    }

    // 要求類提供 formatDate 方法
    abstract protected function formatDate(\DateTime $date): string;
}

class Article {
    use Timestampable;

    private \DateTime $createdAt;

    public function __construct() {
        $this->createdAt = new \DateTime();
    }

    protected function formatDate(\DateTime $date): string {
        return $date->format('Y-m-d H:i:s');
    }
}

Trait 中的私有屬性

trait Singleton {
    private static ?self $instance = null;

    public static function getInstance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {}
}

⚠️ 注意:Trait 的私有屬性屬於使用它的類,不同類之間不共享。


五、Trait vs 繼承 vs 接口

特性 Trait 繼承(Abstract Class) 接口(Interface)
目的 代碼複用 共享實現 + 類型關係 定義行為契約
多用 ✅ 一個類可用多個 Trait ❌ 單繼承 ✅ 可實現多個接口
屬性 ✅ 可含屬性 ✅ 可含屬性 ❌ 不能有實例屬性
構造函數 ❌ 不能定義 __construct ✅ 可定義 ❌ 不能
類型檢查 instanceof Trait 無效 instanceof Parent 有效 instanceof Interface 有效

💡 使用建議

  • 需要類型判斷?→ 用抽象類或接口
  • 只需功能注入?→ 用 Trait
  • 不確定?→ 優先組合 + 接口,Trait 作為補充

六、實戰:構建可複用的驗證與日誌 Trait

<?php
declare(strict_types=1);

// 驗證 Trait
trait Validatable {
    private array $errors = [];

    public function addError(string $field, string $message): void {
        $this->errors[$field][] = $message;
    }

    public function hasErrors(): bool {
        return !empty($this->errors);
    }

    public function getErrors(): array {
        return $this->errors;
    }

    abstract public function validate(): bool;
}

// 日誌 Trait
trait Loggable {
    protected function log(string $level, string $message): void {
        error_log("[$level] " . static::class . ": $message");
    }
}

// 用户類組合兩者
class User {
    use Validatable, Loggable;

    public string $email;

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

    public function validate(): bool {
        if (!filter_var($this->email, FILTER_VALIDATE_EMAIL)) {
            $this->addError('email', '郵箱格式無效');
            $this->log('WARN', '郵箱驗證失敗: ' . $this->email);
            return false;
        }
        return true;
    }
}

// 使用
$user = new User("invalid-email");
if (!$user->validate()) {
    print_r($user->getErrors());
}

今日小練習

  1. 創建一個 Timestampable Trait,自動管理 createdAtupdatedAt 屬性,並提供 getCreatedAt() 方法。
  2. 編寫一個 Cacheable Trait,為類添加簡單的內存緩存功能(使用靜態數組)。
  3. (挑戰)讓 User 類同時使用 JsonSerializableTrait(第12天)和 Validatable Trait,並實現完整註冊流程。

明日預告:第14天 —— 命名空間(Namespace)與自動加載(Autoloading)

明天我們將學習如何組織大型項目代碼:

  • 什麼是命名空間?為什麼需要它?
  • namespaceuse 的正確用法
  • PSR-4 自動加載標準
  • Composer 如何幫你自動加載類

告別 require_once 地獄,邁向現代化 PHP 開發!


結語

Trait 是 PHP 對“多重繼承”需求的優雅迴應。它不是銀彈,但在合適場景下能極大提升代碼複用性。記住:Trait 是工具,不是架構——過度使用會導致類職責不清。

你已掌握 OOP 的三大支柱(封裝、繼承、多態)以及兩大擴展機制(接口、Trait)。接下來,我們將學習如何組織這些類到大型項目中。

堅持就是勝利!別忘了提交你的代碼,有問題隨時留言。


#PHP84 #Trait #代碼複用 #OOP #水平復用 #Web開發 #編程入門