PHPのトレイト衝突をinsteadofとasで解決する
トレイトの衝突解決とは、同名メソッドを持つ複数のトレイトを1つのクラスで使うときに、insteadofでどちらを使うか選び、asで別名を付ける仕組みです。
トレイトは便利なんですが、2つ使うと大体どこかでメソッド名がかぶります。特にログ機能とか、バリデーション系のちょっとしたヘルパーをトレイトで量産していると、いつか必ずぶつかる。今日はその衝突を止めずに解決する書き方の話です。
何もしないと致命的エラーになる
2つのトレイトが同じ名前のメソッドを持っていて、それを1つのクラスで両方useすると、PHPは自動でどちらかを選んだりはしません。ここが単一継承のオーバーライドと違うところで、曖昧なまま実行しようとすると致命的エラー(Fatal error)で止まります。
trait Logger
{
public function log(string $message): void
{
echo "[Logger] {$message}\n";
}
}
trait Notifier
{
public function log(string $message): void
{
echo "[Notifier] {$message}\n";
}
}
class Service
{
use Logger, Notifier;
// Fatal error: Trait method log has not been applied,
// because there are collisions with other trait methods on Service
}
コンパイル時(というかクラス定義の読み込み時)に検出されるので、実行してから初めて気づく、という事故は少ないです。ただ、量が増えてから気づくと直すのが地味に面倒なので、最初から意識しておいたほうが楽です。
insteadofはどう書く?
insteadofは「このメソッドについては、Aじゃなくてこっちのトレイトのを使う」と明示する演算子です。use文をブロックで書き、その中に解決ルールを並べます。
class Service
{
use Logger, Notifier {
Logger::log insteadof Notifier;
}
}
$service = new Service();
$service->log('start'); // [Logger] start
「Notifier::log ではなく Logger::log を使う」という意味です。insteadofの右側には、除外したいトレイトを複数並べることもできます(Logger::log insteadof Notifier, AnotherTrait;のように)。ここで選ばれなかった側のメソッドは、クラスからは使えなくなります。
両方使いたいときはasで別名を付ける
insteadofだけだと片方が消えてしまうので、「両方の実装を残したい」場合はasでエイリアスを作ります。元のメソッドの可視性はそのままに、別名で両方呼べるようにする書き方です。
class Service
{
use Logger, Notifier {
Logger::log insteadof Notifier;
Notifier::log as notify;
}
}
$service = new Service();
$service->log('start'); // [Logger] start
$service->notify('start'); // [Notifier] start
これでlog()はLoggerの実装、notify()はNotifierの実装として両方生き残ります。名前がかぶっただけで、本当は両方使いたかった、というケースは実務でも普通にあるので、insteadofとasはセットで覚えておくと使いやすいです。
asは可視性を変えるためだけにも使える
asは別名を付けるだけでなく、メソッドの可視性(public/protected/private)を変える目的だけでも使えます。この場合は別名を省略して、可視性キーワードだけを書きます。
trait Greeting
{
public function hello(): string
{
return 'hello';
}
}
class Service
{
use Greeting {
hello as protected;
}
}
元のトレイトではpublicだったhello()を、Service側ではprotectedに絞り込んでいます。トレイトは「外に公開する前提」で書かれていることも多いので、使う側のクラスで可視性を締め直したいときに地味に役立ちます。
3つ以上使うとどうなるか
トレイトが3つ4つと増えても考え方は同じで、衝突するメソッドごとにinsteadofを1行書いていくだけです。ただし、あるメソッドについて解決ルールを書き忘れると、そこだけ致命的エラーが残るので、エラーメッセージに出てくるメソッド名を見ながら1つずつ潰していく地道な作業になります。個人的には、トレイトを3つ以上組み合わせる設計になった時点で、一度「本当にトレイトである必要があるか」を見直したほうがいいと思っています。継承や委譲で素直に書けるなら、そちらのほうが衝突解決のルールを覚えなくて済む分、読みやすいことが多い気がしています。
まとめ
複数のトレイトで名前がぶつかったら、PHPは黙って選んでくれるわけではなく、insteadofで採用するトレイトを明示し、必要ならasで別名や可視性を付け直す、という2段構えで解決します。エラーメッセージも比較的親切なので、慌てずに1つずつルールを足していけば迷うことはないはずです。ただ、衝突解決のルールが増えてきたときは、設計そのものを見直すサインかもしれません。
よくある質問
Q. insteadofとasは同時に使えますか?
A. 使えます。同じuseブロックの中に、insteadofの行とasの行を並べて書けます。むしろ「片方を採用しつつ、もう片方を別名で残す」という組み合わせが実務では一番多いパターンです。
Q. 継承したクラスのメソッドとトレイトのメソッドが同名だったらどうなりますか?
A. その場合はエラーになりません。トレイトのメソッドは、継承元(親クラス)のメソッドより優先されます。トレイト同士がぶつかったときだけ、insteadofによる明示的な解決が必要になります。
Q. asで付けた別名は、元のメソッド名の代わりになりますか?
A. いいえ、置き換えではなく追加です。元の名前(この例ではlog)は使えなくなりますが、asで指定した別名(notify)が新しく使えるようになる、という形で両方の実装にアクセスできます。