PHP入門 オブジェクト指向 トレイト

トレイト(Trait)とは、再利用可能なコード(メソッド/プロパティ)をまとめて切り出しておくための仕組みです。「断片的なクラス」と言ってもよいかもしれません。トレイトとして切り出したコードは、あとから個々のクラスに取り込むことが可能です。

トレイトの基本

MachineTrait.php

<?php
trait MachineTrait{
    private string $starting = 'Starting...Run';

    //機器を起動
    public function run(): void{
        print $this->starting;
    }

}
<?php
require_once 'MachineTrait.php';

class Fax{
    //MachineTraitトレイトをインポート
    use MachineTrait;
    //Faxを送信
    public function send(): void{
        print 'sending Fax...sended!';
    }
}

$fx = new Fax();
$fx->run();
$fx->send();

これでMachineTraitトレイトがFaxクラスに組み込まれました。確かに、MachineTraitトレイトで定義されたrunメソッドをFaxクラス経由で呼び出せることが確認できます。Faxクラスで定義されたsendメソッドも問題なく呼び出せます。

実行結果
実行結果

トレイトを定義するには、trait命令を利用します。

構文:trait命令

trait トレイト名{
    ・・・・プロパティ/メソッドの定義・・・・
}

traitブロック配下の構文は、ほぼclassブロックにそれに準じします。ただし、以下の制約があります。

  • 定数は持てない(プロパティ、抽象/静的/インスタンスメソッドのみ可能)
  • クラスの継承、インターフェースの実装はできない

また、トレイト自体は不完全なクラスなので、トレイトそのものをインスタンス化することはできません。トレイトの機能を利用するには、クラスの内部でuse命令を利用します。カンマ区切りで列挙することで、複数のトレイトをまとめて取り込むこともできます。

構文:use命令

use trait,・・・・
traitトレイト名

トレイトと多重継承

FaxPrinterクラスのようにFax/Printerクラス双方の特性を引き継ぐ場合、PHPでは単一継承ですから、複数のクラスを同時に継承することはできません。では、インターフェースはどうかというと、インターフェースは多重継承ができますが、インターフェースは実装を持たない型の宣言にすぎないので、実装そのものを再利用することはできません。

トレイトを利用することで、Fax/Printerクラス双方の特性を引き継ぐことができます。

<?php
//型の定義
interface IFax{
    function send();
}

interface Iprinter{
    function print();
}

//実装を定義
trait FaxTrait{
    public function send():void{
        print 'sending Fax…sended!';
    }
}
trait PrinterTrait{
    public function print():void{
        print 'printing...completed';
    }
}

//複合機クラスの定義
class FaxPrinter implements IFax,Iprinter{
    use FaxTrait,PrinterTrait;
}

$fp=new FaxPrinter();
$fp->send();
$fp->print();
実行結果
実行結果

これでFax/Printerクラス双方の特性を引き継ぐFaxPrinterクラスを定義できました。ここでポイントはインターフェースを準備している部分です。そのままトレイトをインポートするだけでもコードは正しく動作します。インターフェースを定義しているのは、トレイトは実装を表すだけで型を表現できないためです。トレイトは型宣言には使えません。

アクセサーメソッドの実装

トレイトは、クラスの階層構造とは独立した振る舞いを共有する場合にも利用できます。たとえば、プロパティを隠ぺいするアクセサーメソッドの記述は単純ですが、プロパティの個数が増えればコードが冗長になります。そこで、値を受け渡しする汎用的なアクセサーメソッド機能を共通的に切り出してみます。しかし、アクセサーメソッド機能を、継承でクラス階層の中に組み込むには無理があります。まうz、アクセサーメソッドを継承したことで、他の本来継承すべきクラスを継承できなくってしまいます。そしてなにより、is-a関係が成り立たない間柄を継承で露明日のは、クラスの役割がわかりにくくなる原因でもあります。しかし、トレイトとして切り出すことで、継承関係から独立して実装コードをクラスに付け加えることができます。

<?php
trait AccessorTrait{
    //未定義のプロパティを設定すると、配列propsに値を設定
    public function __set(string $name, mixed $value): void{
        //キーの有無をチェックし、存在しないキーはエラー
        if($this->props[$name]){
            $this->props[$name] = $value;
        }else{
            throw new Exception("{$name}プロパティは存在しません。");
        }
    }

    // 未定義のプロパティを取得すると、配列 propsから値を取得
    public function __get(string $name): mixed { 
        // キーの有無をチェックし、 存在しないキーはエラー
        if ($this->props [$name]) {
            return $this->props[$name];
        } else {
            throw new Exception("{$name}プロパティは存在しません。");
        }
    }
}

class MyTriangle {
    // トレイトを有効化
    use AccessorTrait;
    // プロパティを連想配列として準備
    private $props = [
        'base' => 1,
        'height' => 1
    ];

    public function getArea(): float {
        return $this->base * $this->height / 2;
    }
}

$cls= new MyTriangle();
$cls->base=10;
$cls->height = 5;
print $cls->getArea();// 結果:25

名前競合時の挙動

クラス/トレイトでそれぞれ名前が衝突した場合の優先順位について解説します。

現在のクラス/親クラスと衝突した場合

現在のクラス/親クラスとトレイトが名前衝突した場合、優先順位は以下の通りです。

  1. 現在のクラスのメンバー
  2. トレイトのメンバー
  3. 親クラスのメンバー

<?php
class MyParent {
    public function hoge(): void { 
        print 'MyParent!!';
    }
}

trait MyTrait {
    public function hoge(): void {
        print 'MyTrait!!';
    }
}

// MyParent クラス、 MyTraitクラスを継承 
class MyChild extends MyParent { 
    use MyTrait;
    public function hoge(): void {
        print 'MyChild!!';
    }
}
$cls = new MyChild();
$cls->hoge(); //結果: MyChild!!

まずは、MyChildクラスのhogeメソッドが優先されて、「MyChild!!」が表示されます。

トレイト同士で衝突した場合

トレイト同士で名前が衝突した場合には、エラーが発生します。一般的にトレインと内のメンバーは競合しないよう、名前設定すべきですが、insteadof/as演算子を利用することで、競合そのものを強制的に回避することができます。

1、insteadof演算子で有効化するメンバーを選択

insteadof演算子を利用することで、競合したメンバーのいずれか1つを優先して有効化できます。

class MyClass{
  use MyTrait1,MyTrait2{
    MyTrait1::hoge insteadOf MyTrait2;
  }
}

これでMyTrait2の代わりに、MyTrait1::hogeメソッドを利用しなさいという意味になります。

2、as演算子でメンバーに別名を付与

1、の方法では、複数あるトレイトのいずれか1つのメンバーしか有効にできません。双方とも有効にしたい場合には、as演算子で別名を付与してください。

class MyClass{
  use MyTrait1,MyTrait2{
    MyTrait1::hoge insteadOf MyTrait2;
    MyTrait2::hoge as foo;
  }
}

$cls = new MyChild();
$cls->hoge();
$cls->foo();

確かに、hoge/fooメソッドで、MyTrait1/2それぞれのメンバーを呼び出せることが確認できます。ただし、as演算子を利用した場合にも、insteadof演算子で最初に競合を回避しておかなければなりません。

as句を利用することで、メソッドのアクセス権限を変更することも可能です。たとえば以下は、MyTrait1トレイトのhogeメソッドをprivate権限に変更します。

use MyTrait1 {hoge as private;}

以下のように、エイリアスを付与しつつ、権限を同時に変更することも可能です。

use MyTrait1 {hoge as private foo;}