PHPのトレイトは、再利用可能なメソッドとプロパティのかたまりです。trait Name { ... }で宣言し、クラスの本体の中でuse Name;と書けば、どのクラスにも取り込めます。PHPはトレイトのメンバーをクラスにコピーするので、そこに書いたのとまったく同じように動きます。
UserとRobotには共通の親がありませんが、どちらもhello()を持つようになりました。トレイトはこのためにあります。PHPのクラスは1つのクラスしか継承できないので、トレイトは関係のないクラスの間で、横方向にコードを共有します。
プロパティを持つトレイト
トレイトはメソッドと一緒にプロパティも持ち込めます。いったんトレイトがコピーされればそれはクラスのメソッドなので、メソッドはクラスのほかのメソッドと同じように$thisを使います。
クラスは、両方の宣言がまったく同じ(同じ可視性、型、デフォルト値)でない限り、トレイトのプロパティと同じ名前のプロパティを宣言できません。そうでなければ致命的エラーになります。
複数のトレイトを使う
1つのuse文にカンマで区切って複数のトレイトを並べるか、複数のuse行を書きます。クラスはそのすべてのメンバーを得ます。
insteadofとasで衝突を解決する
2つのトレイトが同じ名前のメソッドを定義していると、PHPは推測を拒否して致命的エラーで止まります。useブロックの中で、insteadofが勝つほうを選び、asが負けたメソッドを別の名前で残します。
ブロックなしの単なるuse EnglishGreeting, FrenchGreeting;では、何も出力されないうちにスクリプトが止まります。
PHP Fatal error: Trait method FrenchGreeting::greet has not been applied as Host::greet, because of collision with EnglishGreeting::greet in /home/index.php on line 12
asで可視性を変えることもできます。use Loggable { log as protected; }はこのクラスでだけトレイトのlog()をprotectedにし、use Loggable { log as private writeLog; }はprivateな別名を加えます。
トレイトの抽象メンバーと静的メンバー
トレイトは抽象メソッドを宣言して、使う側のクラスに何かを求められます。静的メソッドや静的プロパティも持てます。トレイトを使う各クラスは、静的プロパティの自分用のコピーを得ます。
静的プロパティが各クラスにコピーされるので、CarとBikeは別々に数えます。すべての子が共有する、共通の親クラスで宣言した静的プロパティとは違います。その場合はstaticのページで紹介しています。
どのメソッドが優先されるか:クラス、トレイト、親
同じ名前のメソッドがクラス自身、使っているトレイト、親クラスにあるとき、PHPはクラス自身のメソッド、トレイトのメソッド、親のメソッドの順に選びます。トレイトの中からparent::を使えば、今まで通り親の版に届きます。
トレイトは型ではない
instanceofがトレイトに一致することはなく、トレイト名で型宣言した引数はすべてのオブジェクトをTypeErrorで拒否します。「このクラスはその振る舞いを持っているか」を確かめるには、トレイトをインターフェースと組み合わせるか、class_uses()でクラスを調べます。
上のパターンが定番です。インターフェースExportableは関数が型宣言しinstanceofが確認するもので、トレイトExportsPropertiesはクラスが使ったり置き換えたりできる出来合いの実装です。トレイト単体は約束のない共有コードなので、ビジネスロジックでclass_uses()を確認している自分に気づいたら、そのトレイトには隣にインターフェースが必要だったのです。関連するクラスのファミリーで共有するコードなら、抽象クラスのほうが簡単な選択かもしれません。
よくある質問
PHPのトレイトとは何ですか?
トレイトは、trait Name { }で宣言する名前付きのメソッド(とプロパティ)のグループで、クラスはuse Name;でそれを取り込みます。PHPはトレイトのメンバーをクラスにコピーするので、そこに書いたかのように動きます。共通の親を持たないクラスの間でコードを共有する方法です。
PHPのクラスは複数のトレイトを使えますか?
使えます:use Timestamps, SoftDeletes;、または複数のuse行です。2つのトレイトが同じ名前のメソッドを定義していれば、insteadofでどちらかを選ぶ必要があります。たとえばuse A, B { A::hello insteadof B; }で、もう一方はB::hello as helloB;で新しい名前のまま残せます。
PHPのトレイトとインターフェースの違いは何ですか?
インターフェースはコードのない型で、クラスが持つべきメソッドを示し、instanceofも使えます。トレイトは型のないコードで、メソッドの本体をクラスに貼り付けますが、$obj instanceof SomeTraitは常にfalseです。型にはインターフェース、デフォルトの実装にはトレイト、という組み合わせでよく使われます。
トレイトはコンストラクタや抽象メソッドを持てますか?
どちらも持てます。トレイトはabstract public function name(): string;と宣言して、使う側のクラスにそのメソッドの提供を求められます。__constructも定義できますが、トレイトのコンストラクタはクラス自身のものと衝突しやすく、よい考えであることはまれです。
クラスとトレイトが同じメソッドを定義していると、どちらが優先されますか?
クラス自身のメソッドがトレイトのものより優先され、トレイトのメソッドは親クラスから受け継いだメソッドより優先されます。つまり、クラス、トレイト、親の順です。