PHPのTrait(トレイト)とは。これまでクラスで解決していた私が有効性を考えてみた

PHP基礎 トレイト(Trait) とは? 基礎
※当サイトはアフィリエイト広告を掲載しています。

PHPのトレイトは、PHP 5.4で導入された比較的以前よりある機能です。もちろん、私もLaravelなどではよく使っています。

が、Laravelの枠組み内だからこそ使っている感が拭えず……。その証拠に、独自Webアプリケーションでは全くと言って良いほど使っていません。

結構、私と同じような方もいらっしゃるのではないでしょうか!?

本ページは、PHPのトレイトの解説を通じ、私自身も理解を深める目的で作りました。

トレイトが初めての方はもちろん、ちゃんと理解しておきたいという方の参考にしていただければ幸いです。

トレイトとは!?

概要

トレイト(Trait)とは、複数のクラスで同じメソッドやプロパティを共有するための機能です。

PHPは単一継承しかサポートしていないため、その不便を軽減するために導入されたようです。

辞書で調べたところ、Trait=特性、特色、特徴という意味を持っているようです。特性を追加できる機能……と考えると覚えやすいかもしれません。

トレイトを定義

トレイトを定義する際には、classキーワードの代わりにtraitキーワードを使用します。

コードを見た方が早いですね。

以下では、トレイトクラスであるExampleTraitにプロパティとメソッドを定義しています。

trait ExampleTrait {
    public $greeting = "Hello from Trait!";

    public function sayHello() {
        echo $this->greeting;
    }
}

トレイトの使用

定義したTraitをクラスで使用する際には、useキーワードを用います。以下は先ほどのトレイトをMyClassで利用しています。

class MyClass {
    use ExampleTrait;
}

$obj = new MyClass();
$obj->sayHello();  // 出力: Hello from Trait!

このようにして、トレイト内のプロパティやメソッドをクラスに取り込み、そのクラスのインスタンスからアクセスすることができます。

抽象クラスに近い!?

トレイトは、直接インスタンス化することができません。

trait ExampleTrait {
    public $greeting = "Hello from Trait!";

    public function sayHello() {
        echo $this->greeting;
    }
}
// 以下はできない
// $trait = new ExampleTrait();

後に考えを改めることになりますが、最初は抽象クラスに近いのかなと当初の私は感じました(実際、後述しますが、抽象メソッドも定義できます)。

しかしながら、トレイトには継承機能がありません。他のトレイトの利用はできます。

いろいろな使い方

先ほどはベーシックな例を挙げましたが、いろいろな使い方ができます。

複数のトレイトを使用する

複数のトレイトを、一つのクラスで使用することもできます。

trait TraitOne {
    public function methodOne() {
        echo "Method from TraitOne";
    }
}

trait TraitTwo {
    public function methodTwo() {
        echo "Method from TraitTwo";
    }
}

class MyClass {
    use TraitOne, TraitTwo;
}

$obj = new MyClass();
$obj->methodOne();  // 出力: Method from TraitOne
$obj->methodTwo();  // 出力: Method from TraitTwo

このように、,でくぎることで複数のトレイトを利用することが可能です。

トレイト間で競合するメソッドがある場合

複数のトレイトを一つのクラスで使用する際、同名のメソッドが存在すると競合が発生し、Fatal errorとなります。

この場合、insteadof キーワードを使い、どのメソッドを使用するか明示的に指定する必要があります。

trait TraitA {
    public function conflictingMethod() {
        echo "From TraitA";
    }
}

trait TraitB {
    public function conflictingMethod() {
        echo "From TraitB";
    }
}

class MyClass {
    use TraitA, TraitB {
        TraitA::conflictingMethod insteadof TraitB;
    }
}

この例では、MyClass では TraitAconflictingMethod が使用され、TraitB のものは除外されます。

メソッドのエイリアスとしての使用

as キーワードを使用して、取り込むメソッドの名前のエイリアス(別名)設定することも可能です。

trait TraitExample {
    public function exampleMethod() {
        echo "From Trait";
    }
}

class MyClass {
    use TraitExample { exampleMethod as aliasMethod; }
}

$obj = new MyClass();
$obj->aliasMethod();  // 出力: From Trait
$obj->exampleMethod();  // 出力: From Trait

13行目を見ると分かるとおり、あくまでエイリアスの「追加」です。元の名称での利用ができなくなるわけではありません。

抽象メソッドを使う

抽象クラスと同様、トレイトも抽象メソッドを実装することが可能です。

抽象メソッドを持つトレイを使用するクラスでは、その抽象メソッドに対する具体的な実装を提供する必要があります。

もし実装し忘れると、Fatal errorが発生します。

trait Printable {
    // 抽象メソッドの定義
    abstract public function getContent();

    public function printContent() {
        echo $this->getContent();
    }
}

class Article {
    use Printable;

    private $content;

    public function __construct($content) {
        $this->content = $content;
    }

    // 抽象メソッドの実装
    public function getContent() {
        return $this->content;
    }
}

$article = new Article("Hello, World!");
$article->printContent();  // 出力: Hello, World!

上記の例では、Printable トレイトに抽象メソッド getContent を定義しています。

Article クラスはこのトレイトを使用しているので、getContent の具体的な実装をする必要があります。

クラスとの違い・使い分けが分からない!?

トレイトは、「継承できない&同時にいくつも利用できる抽象クラス」のようなイメージを持たれる方も多いかもしれません。

正直な所、クラスとどう使い分ければ良いのか。どういった利点があるのか今一掴みづらいところです。

そこでトレイトの考え方を、自分なりに解釈してみました。

1トレイト=1ファイル=1機能

トレイトは1つのファイルに1機能(役割)として記述し、その機能に関しては完結するものとして作った方が良さそうです。

そうすれば、その機能を複数のクラスで使うことができます。

クラスとは別軸として、独立した機能としての便利さを感じました。

クラスも1クラス1機能にすることは同じではありますが、クラスは親子の関係を持つことが多いです。それに対しトレイトは、親子は無く水平方向で機能を実装できるイメージです。

“Does-a”または”Has-a”関係

クラスは”Is-a”関係。つまりは「〜は〜である」という関係で継承します。DogAnimalであるため、DogクラスがAnimalクラスを継承する……という考えです。

対するトレイトは、”Does-a”または”Has-a”関係、つまり「〜が〜の機能を持つ/行う」感じで使うもの、と考えるのが良さそうです。

例えばログの機能を提供するためのLoggerTraitを多くのクラスに実装したい……という場合はトレイトが好ましいかもしれません。

逆にis-aの関係でなくても機能を追加できる、という風に捉えると良いかもしれません。これに関しては事項で出てきます。

実用的なコード例

私は過去にキャッシュに関する独自クラスを作ったことがありました。

その簡易版のようなクラスを例に、トレイトに書き直してみたいと思います。

こんなクラスです。

  • キャッシュを管理するクラス
  • キャッシュグループを利用できる(グループ毎に読み込み・保存を分ける)
  • DBの結果などをはじめ、いろいろなキャッシュを保存する用途を想定

修正前

何の変哲も無い、キャッシュを管理するクラスです。

class Cache {
    private $cacheGroup;

    public function __construct(string $group) {
        $this->cacheGroup = $group;
    }

    public function setCache($key, $value) {
        echo "Saving {$value} to cache with key {$key} in group {$this->cacheGroup}\n";
        // 実際のキャッシュ保存処理はここに...
    }

    public function getCache($key) {
        echo "Fetching data from cache with key {$key} in group {$this->cacheGroup}\n";
        // 実際のキャッシュ取得処理はここに...
        return "Sample Data";  // デモ用のデータ
    }

    public function clearGroupCache() {
        echo "Clearing cache in group {$this->cacheGroup}\n";
        // 実際のキャッシュグループのクリア処理はここに...
    }
}

これを利用するクラスが以下です。


class User {
    private $cache;

    public function __construct() {
        $this->cache = new Cache("user_group");
    }

    public function setCache($key, $value) {
        $this->cache->setCache($key, $value);
    }

    public function getCache($key) {
        return $this->cache->getCache($key);
    }

    public function clearCache() {
        $this->cache->clearGroupCache();
    }
}

class Product {
    private $cache;

    public function __construct() {
        $this->cache = new Cache("product_group");
    }

    public function setCache($key, $value) {
        $this->cache->setCache($key, $value);
    }

    public function getCache($key) {
        return $this->cache->getCache($key);
    }

    public function clearCache() {
        $this->cache->clearGroupCache();
    }
}

このようなCacheクラスだと、キャッシュ機能をつけたいだけなのに、利用するクラスごとにsetCache(), getCache()などの実装をしています。

getCache()などを作ってインスタンスごと返してやればそんな実装は不要ですが、カプセル化したいです。

もしくはCacheクラスを継承すれば良いですが、”is-a”の観点から矛盾してしまいますのでやってはいけません。

Traitを使った例

こんな時にトレイトが使えそうです。

まずはキャッシュができるトレイト(特性)ということで、Cachebleと名前を変えて、以下の様にしました。

trait Cacheable {
    abstract protected function getCacheGroup(): string;

    public function setCache($key, $value) {
        $group = $this->getCacheGroup();
        echo "Saving {$value} to cache with key {$key} in group {$group}\n";
        // 実際のキャッシュ保存処理はここに...
    }

    public function getCache($key) {
        $group = $this->getCacheGroup();
        echo "Fetching data from cache with key {$key} in group {$group}\n";
        // 実際のキャッシュ取得処理はここに...
        return "Sample Data";  // デモ用のデータ
    }

    public function clearGroupCache() {
        $group = $this->getCacheGroup();
        echo "Clearing cache in group {$group}\n";
        // 実際のキャッシュグループのクリア処理はここに...
    }
}

UserクラスとProductクラスを作成します。これらのクラスは上記のCacheable トレイトを使用します。

class User {
    use Cacheable;

    protected function getCacheGroup(): string {
        return "user_group";
    }
}

class Product {
    use Cacheable;

    protected function getCacheGroup(): string {
        return "product_group";
    }
}

簡単ですね!これだけで両クラスにキャッシュ機能が追加されました。クラスが増えても、手間はあまりかかりません。

こんな感じで使います。

$user = new User();
$user->setCache('userId_123', '山田太郎');
$user->getCache('userId_123');
$user->clearGroupCache();

$product = new Product();
$product->setCache('productId_456', 'おいしい羊羹');
$product->getCache('productId_456');
$product->clearGroupCache();

適所であれば、クラスよりも柔軟なコードが書けますね。

まとめ

以上、PHPのトレイとの説明でした。

昔からの癖で、ついついクラスで解決するコーディングスタイルが身についてしまい、自作のWebアプリケーションではさっぱりでした。

しかし、今回トレイトを研究することで、改めてトレイト良いなと。しかも、内容が身についた気がします。

今後はLaravel以外でも、トレイトの機能をもっと使っていきたいと思います。

この記事が、少しでもあなたの役にも立てばうれしいです。

コメント

タイトルとURLをコピーしました