【PHP】trait(トレイト)の使い方|コードの再利用と多重継承の代替パターン

複数の Eloquent モデルに同じローカルスコープやミューテータをコピペしていませんか。筆者も、共通条件を複数モデルへそのまま写したあと、片方だけ修正漏れがあり、本番では期待と異なる挙動になった経験があります。チーム開発でも同様の抜けは起こり得ます。共通処理を親クラスに無理に寄せると継承が肥大化しますし、PHP は単一継承のため別ルートの振る舞いをまとめて取り込むにも限度があります。

こうした「横方向の再利用」の代表的手段が trait(トレイト)です。本記事では trait の基本、insteadofas による名前衝突の解消、Laravel での馴染みのある例、そしてはまりどころまで整理します。

動作の細部は PHP のバージョンによって異なる場合があります。コード例は PHP 8.x を想定します。最新の定義は PHP 公式マニュアルの trait の節 を参照してください。

はじめに

この記事で扱う範囲は次のとおりです。

  • trait の定義とクラスへの取り込み(use
  • メソッドの優先順位と名前の衝突、insteadofas
  • プロパティや抽象メソッドとの組み合わせ時の注意
  • よくある失敗例と次のアクション

trait とは

trait は再利用したいメソッドやプロパティをまとめたグループです。クラス宣言のなかで use トレイト名 と書くと、その trait が提供するメンバーがクラスに取り込まれます。

PHP は 複数クラスからの直接継承(多重継承)はできません。trait 自体が多重継承ではなく、マニュアルでも「水平方向のコード再利用」に触れています。複数の trait を 1 つのクラスへ組み合わせることはできます。

基本的な使い方

trait の定義

trait Timestampable
{
    public function touch(): void
    {
        $this->updatedAt = new DateTimeImmutable();
    }
}

クラスで取り込む

class Article
{
    use Timestampable;

    private DateTimeImmutable $updatedAt;

    public function __construct()
    {
        $this->updatedAt = new DateTimeImmutable();
    }
}

$article = new Article();
$article->touch();

複数の trait を取り込むときは、カンマ区切りで並べます。

class Report
{
    use Timestampable, Loggable;
}

Laravel でよく見る trait

Laravel の Eloquent モデルでは、フレームワークが用意した trait を use するパターンが日常的です。論理削除の SoftDeletes は、その一例です(削除済み行をグローバルスコープで除外する処理などを trait 側にまとめています)。

<?php

namespace App\\Models;

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

class Post extends Model
{
    use SoftDeletes;
}

モデルファクトリを使う HasFactory も trait で提供されています。IDE で use HasFactory; とジャンプすると、trait がメソッドを合成している様子を追いやすいです。

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

class User extends Model
{
    use HasFactory;
}

メソッドの優先順位と名前の衝突

同じ名前のメソッドがクラス本体・親クラス・複数の trait のあいだに現れると、優先ルールが必要になります。PHP のマニュアルでは概ね次のような優先が説明されています。

優先が高い側説明
現在のクラスクラス内で定義したメソッドが最優先
trait親クラスの同名メソッドより trait 側が優先される(クラスが上書きしない場合)
親クラスtrait とクラスが定義していない場合

複数の trait が 同名メソッド を持つ場合、クラス側で解決しないと致命的エラーになります。解決には insteadofas を使います。

insteadof と as の例

trait A
{
    public function save(): void
    {
        echo 'A の保存処理';
    }
}

trait B
{
    public function save(): void
    {
        echo 'B の保存処理';
    }
}

class Model
{
    use A, B {
        B::save insteadof A;
        A::save as saveFromA;
    }
}

$m = new Model();
$m->save();                 // B の save が既定
$m->saveFromA();            // A の save を別名で呼び出し

as別名のエイリアスとしても使えます。ほかに 可視性を変える ためにも使えます(マニュアルの例どおり、publicMethod as protected protectedAlias のような記法)。

trait が public メソッドとして公開している処理を、取り込んだクラスの外部公開 API からは隠したい場合があります。テスト用のフックや内部ヘルパを trait で共有しつつ、クラス利用者には見せない――といったときに、可視性を protected などへ狭める動機になります。

プロパティ・抽象メソッド・静的メソッド

この節では、メソッド以外の trait の要素を短く押さえます。細部は PHP マニュアルの trait の節 に従ってください。

プロパティ

trait にもプロパティを定義できます。取り込むクラス側と 同名のプロパティ を重ねると衝突となり、マニュアルで示されるとおり 可視性と既定値を含めた互換条件 を満たす必要があります。条件を満たさない場合は致命的エラーになります。

trait HasLabel
{
    public string $label = '';
}

class Product
{
    use HasLabel;
}

$p = new Product();
$p->label = '在庫あり';

抽象メソッド

trait は 抽象メソッド を宣言できます。use したクラス側で、そのメソッドを実装する義務が生じます。共通の「契約」と具象実装を分けたいときのパターンです。

trait TimestampFormatter
{
    abstract public function occurredAt(): DateTimeInterface;

    public function occurredAtIso8601(): string
    {
        return $this->occurredAt()->format(DateTimeInterface::ATOM);
    }
}

class OrderEvent
{
    use TimestampFormatter;

    public function __construct(private DateTimeImmutable $at)
    {
    }

    public function occurredAt(): DateTimeInterface
    {
        return $this->at;
    }
}

静的メソッド

trait に 静的メソッド を置けます。取り込んだクラス名から、あたかもクラス自身の静的メソッドのように呼び出せます。

trait TypeName
{
    public static function typeKey(): string
    {
        return strtolower(static::class);
    }
}

class Invoice
{
    use TypeName;
}

Invoice::typeKey(); // "invoice"

実務での使いどころと注意点

trait が検討されやすい場面の例は次のとおりです。

  • Laravel のモデルで共通スコープやヘルパを複数モデルへ広げる場合に向く(フレームワーク側でも trait を利用している)
  • ログ出力や監査フックを継承関係に縛られず複数クラスへ載せたい場合に向く

trait を積み重ねすぎると どの trait がどのメソッドを提供しているか追いにくくなります。利用側クラスを開いても見えない隠れた依存が増える点にも留意してください。

よくある失敗例:衝突を解かずに複数 trait を use する

複数の trait が同じ名前のメソッドを持っているのに、insteadofas で解決しないと、PHP は致命的エラーで停止します。

エラーになる例

trait NamedFoo
{
    public function describe(): string
    {
        return 'foo';
    }
}

trait NamedBar
{
    public function describe(): string
    {
        return 'bar';
    }
}

// Fatal error: Trait method collisions...
class Broken
{
    use NamedFoo, NamedBar;
}

修正後の例

どちらを既定とするかを insteadof で決め、もう一方は as で別名にします。

class Fixed
{
    use NamedFoo, NamedBar {
        NamedBar::describe insteadof NamedFoo;
        NamedFoo::describe as describeFoo;
    }
}

$obj = new Fixed();
$obj->describe();           // bar(NamedBar が優先)
$obj->describeFoo();        // foo

まとめ

  • trait は単一継承を補い、クラスへメソッドなどを横方向に合成する仕組みである。
  • 複数 trait の同名メソッドは insteadofas で解決しなければならない。
  • プロパティ名の衝突や trait の過剰利用による見通しの悪化に注意する。
  • 手元のコードで親クラスへ無理に寄せていた共通処理を trait へ切り出せないか検討するとよい。
  • 関連記事として、Laravel Eloquent のスコープの解説 を読むと trait とグローバルスコープの関係まで踏み込める。

トレイトと インターフェース・抽象クラスの使い分けは、近日、別記事で整理する予定です。

Source

PHPtrait

Posted by 千原 耕司