トレイトで機能を横断的に共有し衝突を解決する
トレイトとは、メソッドやプロパティの断片を複数のクラスに横断的に混ぜ込む(useする)ための仕組みです。単一継承の制約を補って機能を水平共有できます。
多重継承の代わりに機能を混ぜ込む
PHP は単一継承なので、複数の親からメソッドを受け継ぐことはできません。でも「タイムスタンプ管理の機能を、無関係な複数のクラスに持たせたい」ような横断的な関心事はよくありますね。トレイトはそういうときに、コードの断片をクラスに混ぜ込む(use する)ための仕組みです。
trait Timestampable {
private ?string $createdAt = null;
public function touch(): void {
$this->createdAt = date('Y-m-d H:i:s');
}
public function getCreatedAt(): ?string {
return $this->createdAt;
}
}
class Article {
use Timestampable;
}
$a = new Article();
$a->touch();
echo $a->getCreatedAt();
複数トレイトのメソッド名が衝突したら
複数のトレイトを use したとき、同名メソッドがあると致命的エラーになります。これを解決するのが insteadof と as ですね。insteadof でどちらを採用するか決め、as で別名を付けて両方残すこともできます。
trait A { public function hello() { return 'A'; } }
trait B { public function hello() { return 'B'; } }
class C {
use A, B {
A::hello insteadof B; // 基本はAを採用
B::hello as helloFromB; // Bは別名で残す
}
}
$c = new C();
echo $c->hello(); // A
echo $c->helloFromB(); // B
抽象メソッドで実装を強制できる
トレイト側で抽象メソッドを宣言しておくと、use する側にその実装を義務づけられます。トレイトが依存する処理をクラスに委ねつつ、共通ロジックだけを提供する、という設計に使えますね。
trait Loggable {
abstract protected function channel(): string;
public function log(string $msg): void {
echo '[' . $this->channel() . '] ' . $msg;
}
}
class Payment {
use Loggable;
protected function channel(): string { return 'payment'; }
}
まとめ
トレイトは継承とは別軸で、機能の断片を複数クラスに水平展開するための道具ですね。ただ何でも詰め込むと状態の依存が見えづらくなるので、あくまで「振る舞いの共有」に絞って使うのが健全だと思います。衝突は insteadof と as で解決できること、抽象メソッドで契約を強制できることを押さえておくと、実務で困りにくいです。
よくある質問
Q. トレイトと継承はどう使い分けますか?
A. is-aの関係なら継承、無関係な複数クラスに同じ振る舞いを水平展開したいならトレイト、という切り分けが自然です。
Q. 複数トレイトでメソッド名が衝突したら?
A. use句の中でinsteadofどちらを採用するか決め、asで別名を付ければ両方残せます。
Q. トレイトに実装を強制させられますか?
A. トレイト側でabstractメソッドを宣言しておくと、useするクラスにその実装を義務づけられます。