1. ホーム
  2. PHP

【PHP】trait(トレイト)の使い方|複数のクラスで処理を再利用する方法

Share

「ログ出力の処理を、いろいろなクラスで同じように使いたい」——そう思って親クラスにまとめようとしたものの、そのクラスはすでに別のクラスを継承していて手が出せない。PHP を書いていると、こうした場面によく出会います。これを解決するのが trait(トレイト) です。この記事では、trait の基本の書き方から、複数取り込んだときのメンバーの優先順位、同名メソッドが衝突したときの insteadofas による解決、そして interface や継承との使い分けまでを、PHP 8 系の仕様に沿って解説します。

継承だけでは共通処理を配りきれない

PHP のクラスは単一継承です。extends で指定できる親クラスは1つだけで、2つのクラスを同時に継承することはできません。

ng.php
<?php
// これは書けない(PHP は多重継承をサポートしていない)
class ProductController extends BaseController, Loggable
{
}

そのため「すでに BaseController を継承しているクラスに、ログ出力の共通処理も足したい」といった要求は継承では実現できません。かといって同じメソッドを各クラスにコピーして回れば、修正のたびに全部を直すことになります。

この穴を埋めるのが trait です。trait はメソッドやプロパティの実装だけをまとめておき、好きなクラスに何個でも取り込める仕組みで、継承の階層とは無関係に共通処理を配れます。継承ツリーを増やさずに実装を使い回すための、いわば「コードの部品箱」です。

trait の基本の書き方

定義は trait キーワードで行います。中身の書き方はクラスとほとんど同じです。

Loggable.php
<?php
trait Loggable
{
    public function log(string $message): void
    {
        // static::class は、この trait を使っているクラス名になる
        echo '[' . static::class . '] ' . $message . PHP_EOL;
    }
}

取り込む側のクラスでは、クラスの 波括弧の中use トレイト名; と書きます。これだけで、trait のメソッドがそのクラスに定義されているのと同じように使えます。

ProductController.php
<?php
class ProductController
{
    use Loggable;   // trait を取り込む

    public function index(): void
    {
        $this->log('一覧を表示しました');
    }
}

class UserService
{
    use Loggable;   // 同じ trait を別のクラスでも使える
}

(new ProductController())->index();  // [ProductController] 一覧を表示しました
(new UserService())->log('登録完了'); // [UserService] 登録完了

ProductControllerUserService は継承関係がまったくありませんが、同じ log() を共有できています。これが trait の一番の効果です。

名前空間の use とクラス内の use は別物

ここが初心者のつまずきやすいところです。PHP には use というキーワードが複数の意味で登場します。ファイルの先頭に書く use はクラス名のインポート(名前空間の別名定義)で、クラスの中に書く use は trait の取り込みです。名前は同じでも、機能はまったく別物です。

ProductController.php
<?php
namespace App\Controller;

use App\Support\Loggable;   // ← 名前空間のインポート(ファイル先頭)

class ProductController
{
    use Loggable;           // ← trait の取り込み(クラスの中)
}

名前空間を使っているプロジェクトでは、この2つを両方とも書く必要があります。ファイル先頭の use だけでは trait は取り込まれませんし、クラス内の use Loggable; だけでは(同じ名前空間に Loggable が無い限り)クラスが見つからずエラーになります。「先頭の use は名前の解決、中の use は機能の取り込み」と覚えておくと混乱しません。

trait に書けるもの・書けないこと

trait はクラスによく似ていますが、単体でインスタンス化することはできませんnew Loggable() と書くと致命的なエラーになります。あくまで「クラスに取り込まれて初めて動く部品」です。

書けるもの説明
メソッドpublic / protected / private すべて定義できる
プロパティ取り込んだクラスのプロパティになる。同名のプロパティを持つクラスと衝突しないよう注意
抽象メソッドabstract で宣言すると、使う側のクラスに実装を強制できる
静的メソッド・静的プロパティ定義できる。静的プロパティは取り込んだクラスごとに別々の値を持つ
インスタンス化できない(new できるのはクラスだけ)
Countable.php
<?php
trait Counter
{
    private int $count = 0;          // プロパティ
    private static int $total = 0;   // 静的プロパティ

    public function increment(): int // メソッド
    {
        self::$total++;
        return ++$this->count;
    }

    public static function total(): int // 静的メソッド
    {
        return self::$total;
    }
}

class Page  { use Counter; }
class Image { use Counter; }

(new Page())->increment();
(new Page())->increment();
(new Image())->increment();

echo Page::total();  // 2
echo Image::total(); // 1(静的プロパティはクラスごとに独立している)

静的プロパティが PageImage で共有されない点はよく誤解されます。trait のコードは各クラスにコピーされたかのように展開されるため、静的な状態もクラスごとに独立します。

複数の trait を取り込む

継承と違い、trait はいくつでも取り込めます。カンマ区切りで並べるか、use を複数行書きます。

ReportService.php
<?php
class ReportService
{
    use Loggable, Counter;   // カンマ区切りでまとめて取り込む
}

// 下のように分けて書いても同じ
class ReportService2
{
    use Loggable;
    use Counter;
}

さらに、trait の中で別の trait を use することもできます。共通部品を組み合わせて、より大きな部品を作れるということです。

クラス・trait・親クラスの優先順位

同じ名前のメソッドが、クラス自身・trait・親クラスの3か所にあった場合、どれが使われるのでしょうか。PHP の優先順位は次のとおりです。

優先度定義場所
1(最優先)クラス自身が定義したメンバー
2trait から取り込んだメンバー
3親クラスから継承したメンバー

つまり trait は親クラスのメソッドを上書きし、クラス自身のメソッドは trait を上書きします。実際に確認してみましょう。

priority.php
<?php
class BaseController
{
    public function name(): string
    {
        return '親クラス';
    }
}

trait NameTrait
{
    public function name(): string
    {
        return 'trait';
    }
}

// trait だけがある場合 → trait が親クラスに勝つ
class PostController extends BaseController
{
    use NameTrait;
}

// クラス自身にも定義がある場合 → クラス自身が最優先
class PageController extends BaseController
{
    use NameTrait;

    public function name(): string
    {
        return 'クラス自身';
    }
}

echo (new PostController())->name(); // trait
echo (new PageController())->name(); // クラス自身

PostController では親クラスの name() ではなく trait の実装が呼ばれ、PageController では自分自身の実装が呼ばれます。「trait はデフォルトの実装を配るもので、クラス側で必要に応じて差し替えられる」と捉えると分かりやすいでしょう。

同名メソッドの衝突を insteadof と as で解決する

複数の trait を取り込んだとき、同じ名前のメソッドが含まれていると衝突して致命的なエラーになります。優先順位で自動的にどちらかが選ばれる、ということはありません。

conflict.php
<?php
trait FileLogger
{
    public function write(string $message): void
    {
        echo 'ファイルに記録: ' . $message;
    }
}

trait MailLogger
{
    public function write(string $message): void
    {
        echo 'メールで送信: ' . $message;
    }
}

class Notifier
{
    use FileLogger, MailLogger; // Fatal error: 衝突している
}

解決するには、use のあとに波括弧のブロックを付けて、どちらを採用するかを明示します。insteadof は「Bの代わりにAを使う」という指定です。

Notifier.php
<?php
class Notifier
{
    use FileLogger, MailLogger {
        FileLogger::write insteadof MailLogger; // write は FileLogger のものを使う
        MailLogger::write as sendMail;          // 使わない方に別名を付けて残す
    }
}

$notifier = new Notifier();
$notifier->write('保存しました');    // ファイルに記録: 保存しました
$notifier->sendMail('保存しました'); // メールで送信: 保存しました

insteadof で採用する側を決め、as で除外した側に別名を付けています。as を書かなければ MailLogger::write は単に使われなくなるだけですが、別名を付けておけば両方の処理を呼び分けられます。

as は可視性の変更にも使える

as はもうひとつ、メソッドの可視性(public / protected / private)を変える働きも持ちます。「trait 側では public だが、このクラスでは外から呼ばれたくない」というときに使います。

visibility.php
<?php
class ProductController
{
    use Loggable {
        log as protected;            // 名前はそのままで protected にする
        log as protected writeLog;   // 別名を付けつつ可視性も変える
    }
}

$controller = new ProductController();
$controller->log('外から呼ぶ'); // Error: protected なので呼び出せない

別名を書かずに log as protected; とすると、メソッド名は log のまま可視性だけが変わります。別名と可視性を同時に指定することもできます。

抽象メソッドで実装を強制する

trait の中には abstract なメソッドを宣言できます。中身のない宣言だけを置くことで、その trait を使うクラスに実装を義務づけることができます。実装しなければ致命的なエラーになるため、「共通処理は trait 側が持ち、クラスごとに変わる部分だけをクラスに書かせる」という設計が作れます。

Validatable.php
<?php
trait Validatable
{
    // 使う側のクラスに実装を強制する
    abstract protected function requiredFields(): array;

    public function validate(array $input): array
    {
        $errors = [];

        foreach ($this->requiredFields() as $field) {
            if (!isset($input[$field]) || $input[$field] === '') {
                $errors[] = $field . ' は必須項目です';
            }
        }

        return $errors;
    }
}
ContactForm.php
<?php
class ContactForm
{
    use Validatable;

    // trait が要求しているので、必ず実装しなければならない
    protected function requiredFields(): array
    {
        return ['name', 'email', 'message'];
    }
}

$form = new ContactForm();
print_r($form->validate(['name' => '山田', 'email' => '']));
// Array ( [0] => email は必須項目です [1] => message は必須項目です )

検証のロジックは trait に1か所だけ書き、「どの項目が必須か」だけをクラスごとに宣言する形になりました。抽象メソッドを使うと、trait と使う側のクラスの間の「約束」がコードに現れるので、読み手にも意図が伝わります。

実践例:プラグインの共通処理を trait にまとめる

WordPress のプラグインやフレームワークでは、「インスタンスを1つだけ作る(シングルトン)」「ログを出す」といった処理をどのクラスでも同じように書きたくなります。こうした定番の処理は trait にまとめる典型例です。

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

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            // trait の中の self は「取り込んだクラス」を指す
            self::$instance = new self();
        }

        return self::$instance;
    }

    // 外部から new されないようにする
    private function __construct()
    {
        $this->init();
    }

    // 初期化処理はクラスごとに書かせる
    abstract protected function init(): void;
}
my-plugin.php
<?php
class MyPlugin
{
    use Singleton, Loggable;

    protected function init(): void
    {
        add_action('init', [$this, 'registerPostType']);
        $this->log('プラグインを初期化しました');
    }

    public function registerPostType(): void
    {
        register_post_type('book', [
            'label'        => '書籍',
            'public'       => true,
            'show_in_rest' => true,
        ]);
    }
}

MyPlugin::getInstance();

シングルトンの仕組みとログ出力はそれぞれ独立した trait にあり、MyPlugin にはそのプラグイン固有の処理だけが残ります。別のプラグインや別のクラスでも同じ2つの trait を取り込むだけで、同じ土台を再利用できます。Singleton 側で init() を抽象メソッドにしているので、取り込んだクラスは初期化処理を必ず書くことになります。

trait と interface・継承の使い分け

trait・interface・継承はどれも「複数のクラスで共通のものを扱う」仕組みですが、担っている役割は別々です。混同すると設計が歪みやすいので、次の表で整理しておきましょう。

仕組み役割実装を持つか数の制限
interface「このメソッドを持つ」という約束(型)を決める持たない(メソッドの宣言のみ)いくつでも実装できる
trait実装の使い回し。同じコードを複数クラスに配る持ついくつでも取り込める
extends(継承)is-a の関係。「PostController は Controller の一種」という親子関係を表す持つ親クラスは1つだけ

大事なのは、trait は型ではないという点です。use Loggable; したクラスは $obj instanceof Loggable のような型判定の対象にはなりません。「この処理を受け取れる相手か」を型で保証したいなら interface を使い、その interface のメソッドを毎回同じように実装したくないなら trait で中身を配る——このinterface と trait の組み合わせが実務ではよく使われます。

combine.php
<?php
interface LoggerInterface
{
    public function log(string $message): void;
}

// 型は interface で保証し、実装は trait で配る
class ProductController implements LoggerInterface
{
    use Loggable;
}

var_dump(new ProductController() instanceof LoggerInterface); // bool(true)

一方、継承は「子は親の一種である」という関係が成り立つときだけ使います。単にコードを共有したいだけで extends するのは、意味のない親子関係を作ってしまうため避けたほうがよいでしょう。

trait を使いすぎると読みにくくなる理由

trait は手軽なぶん、増やしすぎるとコードが追いにくくなります。設計上の注意点を2つ押さえておきましょう。

どこで定義されたメソッドか分からなくなる

1つのクラスに5個も6個も trait を取り込むと、クラスのファイルを開いても中身がほとんど書かれていない状態になります。$this->format() というコードを見つけても、その実装がどの trait にあるのかはファイルを横断して探すしかありません。継承なら親クラスを1つ辿ればよいのに対し、trait は候補が同時に複数あるぶん追跡の手間が増えます。

目安として、1クラスに取り込む trait は2〜3個までにとどめ、それ以上必要になったら「そのクラスが責務を持ちすぎていないか」を疑ってみてください。多くの場合は、処理を別クラスに切り出して受け取って使う(コンポジション)ほうが素直な形になります。

プロパティを前提にした trait は壊れやすい

trait がクラス側のプロパティを直接読み書きしていると、依存関係がコードの見た目に現れません。たとえば trait の中で $this->options を使っていると、その trait は「options というプロパティを持つクラス」でしか正しく動かないのに、取り込む側からはその条件が見えないのです。別の trait も同じ名前のプロパティを定義していれば、初期値が違う場合に衝突エラーにもなります。

対策はシンプルで、必要な値はプロパティを直接見るのではなく、抽象メソッド経由で受け取ることです。前述の requiredFields() のように abstract で宣言しておけば、trait が何を必要としているかがコード上で明示され、実装漏れもエラーで検出できます。trait は「状態を持たず、渡された情報で処理する」形に寄せておくほど安全です。

まとめ

trait は、単一継承の PHP で共通処理を複数のクラスに配るための仕組みです。trait Loggable { ... } で定義し、クラスの中に use Loggable; と書いて取り込みます。ファイル先頭の名前空間の use とは別物なので、名前空間を使っているなら両方書く必要があります。メンバーの優先順位はクラス自身 > trait > 親クラスで、複数の trait が同名メソッドを持つと衝突エラーになるため insteadofas で解決します。as は可視性の変更にも使えます。抽象メソッドを宣言すれば、使う側のクラスに実装を強制できます。interface は型の約束、trait は実装の使い回し、継承は is-a の関係と役割を分けて考え、trait を増やしすぎないように保てば、見通しのよいコードになります。

参考ページ