【PHP】PHPのトレイト(trait)で複数クラスに機能を共有する方法

phpアイキャッチ PHP

こんにちは、かつコーチです。

前回は静的メソッド・静的プロパティ(static)について解説しました。

今回でオブジェクト指向シリーズはひと区切りです。

最後に扱うのは「trait(トレイト)」という、Laravelを学ぶ上でも避けて通れない仕組みです。

「継承は1つの親クラスしか使えない」という制約を、どう乗り越えるかという話が今回のテーマになります。

traitとは?

継承の限界:多重継承ができない

第22回で解説した継承には、実は大きな制約があります。

PHPのクラスは、extends で継承できる親クラスが1つだけという制約です(これを単一継承と呼びます)。

たとえば「ログ出力機能」と「通知送信機能」という、まったく別々の目的を持つ2つの機能を、1つのクラスに両方持たせたいとします。

継承だけでこれを実現しようとすると、LoggableAndNotifiable のような無理やり両方の意味を持たせた親クラスを作るしかなく、設計として不自然になってしまいます。

traitは「機能の部品」を複数取り込める仕組み

トレイトtrait)とは、複数のクラスに同じメソッドを共有させるための仕組みです。

継承と違い、1つのクラスに複数のトレイトを取り込むことができます。

「クラス同士の親子関係」を作る継承とは違い、トレイトは「機能の部品を必要な分だけ組み込む」というイメージで捉えると分かりやすいです。

traitの基本の書き方

trait キーワードで定義し、use で取り込む

トレイトは trait キーワードで定義し、クラスの中で use キーワードを使って取り込みます。

<?php
trait Loggable
{
    public function log(string $message): void
    {
        echo '[LOG] ' . $message . PHP_EOL;
    }
}

trait Notifiable
{
    public function notify(string $message): void
    {
        echo '[通知] ' . $message . PHP_EOL;
    }
}

class Order
{
    use Loggable;
    use Notifiable;

    public function complete(): void
    {
        $this->log('注文が完了しました');
        $this->notify('お客様に完了通知を送信しました');
    }
}

$order = new Order();
$order->complete();
// [LOG] 注文が完了しました
// [通知] お客様に完了通知を送信しました
?>

Order クラスは LoggableNotifiable という2つのトレイトを use することで、log()notify() の両方のメソッドを、継承を使わずに持てるようになりました。

もし別の場所で User クラスにも同じログ機能を持たせたくなったら、同じように use Loggable; と書くだけで、コードをコピーせずに使い回せます。

traitと継承の違いを比較する

比較表で整理する

「継承」「インターフェース」「トレイト」の違いが混同されやすいので、あらためて表で整理しておきます。

観点継承(extendsインターフェース(implementsトレイト(use
引き継げるものプロパティ・メソッドの実装メソッドの「約束」のみプロパティ・メソッドの実装
1クラスで使える数1つの親クラスのみ複数実装可能複数取り込み可能
主な目的「AはBの一種である」という関係を表す異なるクラス間で振る舞いを保証する関係のないクラス同士で処理を使い回す

継承は「動物→犬」のような明確な親子関係があるときに使うのに対して、トレイトは「ログを出す」「通知を送る」のような、クラス同士の血縁関係とは関係のない共通機能を横断的に持たせたいときに使う、という違いを意識すると使い分けやすくなります。

traitを使うべき場面・避けるべき場面

使うべき場面:横断的な小さい機能

トレイトが向いているのは、複数の無関係なクラスに共通して必要になる、小さく独立した機能です。

たとえば「作成日時・更新日時を自動で記録する」「ソフトデリート(論理削除)機能を持たせる」といった、クラスの本質的な役割とは別の「横断的な機能」がその代表例です。

避けるべき場面:巨大なトレイトの乱用

❌ Before:何でもかんでも1つのトレイトに詰め込む

<?php
trait UserHelper
{
    public function validateEmail(string $email): bool { /* ... */ return true; }
    public function sendWelcomeMail(): void { /* ... */ }
    public function calculateAge(string $birthDate): int { /* ... */ return 0; }
    public function formatAddress(): string { /* ... */ return ''; }
    // 関係の薄い機能がどんどん増えていく…
}
?>

このように目的がバラバラな機能を1つのトレイトに詰め込んでしまうと、「このトレイトは結局何をするものなのか」が分からなくなり、継承の乱用と同じ問題が起きます。

✅ After:目的ごとに小さく分割する

<?php
trait EmailValidatable
{
    public function validateEmail(string $email): bool
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }
}

trait AgeCalculatable
{
    public function calculateAge(string $birthDate): int
    {
        $birth = new DateTime($birthDate);
        $now = new DateTime();
        return $birth->diff($now)->y;
    }
}

class User
{
    use EmailValidatable;
    use AgeCalculatable;
}
?>

トレイトを目的ごとに小さく分割しておくことで、それぞれのトレイトが「何をするものか」が名前だけで分かるようになり、必要なクラスにだけ選んで use できるようになります。

私がつまずいたポイント:同名メソッドの衝突

私が実際に手を止められたのは、2つのトレイトを同じクラスで use したときに、両方に同じ名前のメソッドが定義されていたケースです。

<?php
trait Loggable
{
    public function log(string $message): void
    {
        echo '[LOG] ' . $message;
    }
}

trait AuditLoggable
{
    public function log(string $message): void
    {
        echo '[AUDIT] ' . $message;
    }
}

class Order
{
    use Loggable;
    use AuditLoggable;
}
?>

このコードを実行すると、次のようなエラーが出ました。

Fatal error: Trait method log has not been applied,
because there are collisions with other trait methods on Order

LoggableAuditLoggable の両方に log() メソッドがあるため、PHPは「どちらを使えばいいか判断できない」としてエラーを出します。

このときは insteadof というキーワードを使って、どちらのメソッドを優先するかを明示的に指定することで解決しました。

<?php
class Order
{
    use Loggable, AuditLoggable {
        Loggable::log insteadof AuditLoggable;       // Loggable側のlog()を優先
        AuditLoggable::log as auditLog;               // AuditLoggable側はauditLog()という別名で使う
    }
}

$order = new Order();
$order->log('通常ログ');       // [LOG] 通常ログ
$order->auditLog('監査ログ');  // [AUDIT] 監査ログ
?>

このエラーに初めて出会ったときは何が起きているのか理解できず、しばらくエラーメッセージとにらめっこしていましたが、「複数のトレイトを組み合わせるときは、メソッド名の衝突に注意する」という教訓を得られました。

トレイトを設計するときは、他のトレイトと名前が被りそうにない、具体的なメソッド名を付けることを意識するようになりました。

Laravelとの接点

Laravelを使い始めると、モデルクラスの先頭でよく use HasFactory;use SoftDeletes; という記述を見かけると思います。

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Post extends Model
{
    use HasFactory, SoftDeletes;
}
?>

HasFactory はテスト用のダミーデータを簡単に作る機能を、SoftDeletes はレコードを完全に消さずに「削除済みフラグ」を立てるだけにする機能を、それぞれモデルに追加するトレイトです。

Post クラスは Model クラスを継承しつつ、それとは別にトレイトで機能を追加している、というのがこのコードの構造です。

今回学んだ「継承は1つ、トレイトは複数取り込める」という仕組みが分かっていれば、このLaravelのコードが何をしているか自信を持って読めるはずです。

まとめ

この記事のポイント

  • トレイト(trait)は、継承の「1クラス1親のみ」という制約を超えて、複数の機能をクラスに取り込める仕組み
  • 継承が「AはBの一種」という縦の関係を表すのに対し、トレイトは無関係なクラス間で横断的に機能を共有する
  • 複数のトレイトで同名メソッドがあると衝突エラーになる。insteadofas で優先順位や別名を指定できる
  • トレイトは目的ごとに小さく分割し、何でも詰め込んだ巨大なトレイトを作らないようにする
  • Laravelの HasFactorySoftDeletes も、今回学んだトレイトの仕組みそのもの

次に読むべき記事

→ 次の記事:PHPからMySQLに接続する:PDOの基本

タイトルとURLをコピーしました