株式会社インフィニットループ PHPとスマホアプリ開発を行う札幌のシステム会社

技術ブログ

  1. トップ>
  2. 技術ブログ>
  3. PHP5.4 で実装された trait のまとめと実際の利用例

2014年08月08日 (金)

著者 : 

PHP5.4 で実装された trait のまとめと実際の利用例

弊社技術ブログへお越しのみなさま、こんにちは。今年度入社の新人、 capiba- です。

先日、社内勉強会にて「traitを使って楽したい話」という演目で簡単に trait について発表しました。
trait が実装された PHP5.4 は2年も前にリリースされたものなので、何故今更、という話になると思います。
しかし、ネット上(特に日本語圏)においての trait の記事はまだまだ少なく、具体例を探すのも大変だったので、「もしかして trait はあまり浸透していないんじゃないか?」と考え、 trait の有用性を世に広めるためにこの記事を作り始めました。

今回は、初心者ながら個人的に調べたり考えたりしたことを、

  1. trait とはなにか
  2. trait の実装方法と利用方法
  3. どのようなケースで実装するか
  4. 実装時の小ネタ

の4点に絞ってご紹介しようと思います。

1. trait とはなにか

今回ここでは「何故 trait という機能が搭載されたか」を紹介します。
概要に関しては公式のマニュアルが非常に明快な解説をしているので、是非お読みください。

結論から言うと、 trait は…

実装コードの再利用性を向上させ、アプリケーションの実装コードをよりシンプルにするため

に搭載されました。

そもそも trait が導入された背景は、PHPの「多重継承が出来ない」という問題を解決するという部分にあります。
複数のクラスで大体同じような機能となる、

  • データベースやキャッシュなどの、データへのアクセス
  • SingletonやFactoryなどのデザインパターンの実装
  • モデルクラスのアクセサ

などを、個々のクラスで実装すると…

ソースコードのコピーアンドペーストが増えます。

コピーアンドペーストというのは、ソースコードの再利用性 の観点から出来るだけ使わないようにするのが良いとされています。
※参考 : コードの再利用 – Wikipedia

これまでのPHPでは継承を行うことで実装コードを再利用することが可能でしたが、例えば

「モデルデータの操作が出来て、かつインスタンスを共有したい(Singleton)クラス」

があると、多重継承が出来ないので、どちらかは実装を書くか依存性の注入の実装など少しばかり面倒なことをしなければなりません。
依存性の注入(Dependency Injection) : 今回は紹介しませんが、クラス間の依存関係を疎にするためのデザインパターンの一つです。

そこで出てくるのが trait です。

trait は継承と異なり、クラスとの間に縦方向の関係を構築しません。

継承の関係

継承の関係

トレイトの関係

トレイトの関係

この例が良いか悪いかはさておき、 trait の場合はこのように横方向にいくらでもクラスを拡張することが可能なのです。

実装クラスに「XXを行う機能=振る舞い(メソッドやプロパティ)」を外部実装として追加していくのが trait となります。

インターフェイスを用いれば、「(外部から見て)XXが出来るクラス」というを作ることは出来ます。
しかし、結局「XXが出来るクラス」を実装する際に、毎回その「XXが出来る」ように中身を作らなければなりません(例として、イテレータの実装など)。

もし「XXが出来る」という振る舞いがある程度汎用化されていて、複数の異なるクラスにおいてその振る舞いが使われる場合は、同じ実装を流用したくなります。そんな時に使えるのがこの trait ということです。

※「一定の振る舞いをモジュール化し、クラスに組み込む」という概念は、既に他の言語でも実装されていることがあります。

2. trait の実装方法と利用方法

どんな所で trait を実装すればいいのかと申しますと、ほとんどの場合において

  1. ライブラリの実装
  2. フレームワークの実装

の一部として使われるのではないかと思います。
実装の再利用が目的なので、再利用してもらうライブラリやフレームワークに組み込み、アプリケーションは trait を利用だけする、というスタイルです。なので trait の知識は、アプリケーション開発者よりフレームワーク開発者が知るべきではないかと感じています(もちろん、アプリケーション開発者も、これをどうやって利用するかを知る必要はあります)。

trait 自体の実装はクラスとほとんど同じ書き方で可能です。

<?php
trait SingletonTrait
{
    private static $instance;

    private function __construct() { }

    public static function getInstance()
    {
        if (!isset(self::$instance)) {
            self::$instance = new self();
        }
        return self::$instance;
    }
}

但し、クラスが出来て trait が出来ないこともあります。

  • trait 自体のインスタンス化は不可能(コンストラクタはかける)
  • const による定数宣言は不可能
  • クラスを継承したり、インターフェース実装を宣言出来ない(クラスじゃないので)

その振る舞いを利用するクラス側の実装も簡単です。

<?php
class SomeManager
{
    // シングルトンパターンを利用する
    use SingletonTrait;

    public function processSomething()
    {
        // ...
    }
}

// クラスの利用
$mngr = SomeManager::getInstance();
$mngr->processSomething();

このように、実装クラス側で機能を実装していなくても、 trait を use するだけで使えます。

また、複数の trait を利用する際、 trait 同士でメソッド名が被った場合も、 insteadof, as などのキーワードを使うことで回避することが可能です。

<?php
class SomeClass
{
    use SomeTrait, FooTrait {
        // $this->bar メソッドはSomeTraitのものを使う
        SomeTrait::bar insteadof FooTrait;

        // $this->fooBar メソッドでFooTrait::barを呼ぶ
        FooTrait::bar as fooBar;
    };

    public function someFunc()
    {
        // SomeTrait::barメソッドを呼ぶ
        $this->bar();

        // FooTrait::barメソッドを呼ぶ
        $this->fooBar();

        // 利用内で直接 trait のメソッドやプロパティを呼び出すことは出来ない
        // SomeTrait::bar();
    }
}

※プロパティ(メンバ変数)の宣言では、名前の衝突が許されていません。同じ値を初期化しても E_STRICT エラーが出ます。

/* 使い道が分からないのですが */ trait メソッドの可視性(publicなど)を変更することも可能です。

trait SomeTrait
{
    public function getPublicData()
    {
        // ...
    }
}

class SomeClass
{
    use SomeTrait {
        SomeTrait::getPublicData as private;
    }
}

$c = new SomeClass;
$c->getPublicData();
// PHP Fatal error:  Call to private method SomeClass::getPublicData() from ...

3. どのようなケースで実装するか

実装方法・利用方法がわかったところで、実際どのような所で trait を実装すべきか、という所が重要になってくると思います。
全てではありませんが、私が思いついた実装例をいくつかご紹介します。

3-a. デザインパターンの実装

デザインパターンとは、一定の法則(振る舞い)に従って実装をするという手法ですので、その実装に trait が活用出来ます。

// Composite パターン
trait Compositable
{
    private $childs = [];

    public function addChild($child)
    {
        $this->childs[] = $child;
    }

    public function removeChild($child)
    {
        for ($i = 0; $i < count($this->childs); $i++) {
            if ($this->childs[$i] === $child) {
                unset($this->childs[$i]);
                break;
            }
        }
    }
}

// Template Method パターン
// 比較的制約の多い継承の代わりに trait を利用させる
trait Controller
{
    protected function validateInput()
    {
        // ...
    }

    abstract protected function execute();

    protected function printOutput()
    {
        // ...
    }

    public function process()
    {
        $this->validateInput();
        $this->execute();
        $this->printOutput();
        // ...
    }
}

3-b. interface の基本実装として実装

共通機能の型を宣言する interface ですが、基本的な実装というのはある程度同じであることが多いのではないでしょうか。

そういった場合は、継承によって実装するよりも、 trait を使って実装した方が拡張性に富んだ設計になり得ます。

<?php
// 連想配列でイテレート出来るトレイト
trait HashIteratorTrait
{
    private $it_pos = 0;   // ポジション
    private $it_now = '';  // 現在キー
    private $it_keys = []; // キー配列
    private $it_data = []; // コンテンツ

    private function rewind()
    {
        $this->it_keys = array_keys($this->it_data);
        $this->it_pos = 0;
        $this->it_now = $this->it_keys[$this->it_pos];
    }

    private function current()
    //...
}

class UserList implements Iterator
{
    // Iterator インターフェイスを「連想配列イテレータ」として実装
    use HashIteratorTrait;

    public function __construct($user_id_list)
    {
        $list = [];
        foreach ($user_id_list as $user_id) {
            $list[$user_id] = new User($user_id);
        }
        $this->it_data = $list;
    }

    // ...
}

$user_list = new UserList([100, 101, 102, 103]);
foreach ($user_list as $user_id => $user) {
    // ...
}

3-c. アクセサの実装

C# では public int data { get; private set; } などとして簡単にかけるプロパティへのアクセサですが、PHPでは中々面倒です。

trait でオーバーロードメソッドを実装することで、簡単にアクセサが利用出来ます。

<?php
trait Accessor
{
    private $properties = [];

    // オーバーライドすればセット可否を変えられる
    public function checkSet($name, $value)
    {
        return true;
    }

    public function __set($name, $value)
    {
        if ($this->checkSet($name, $value)) {
            $this->properties[$name] = $value;
        }
    }

    public function __get($name)
    {
        if (!isset($this->properties[$name])) {
            return null;
        }
        return $this->properties[$name];
    }

    public function __isset($name)
    {
        return isset($this->properties[$name]);
    }

    public function __unset($name)
    {
        if (isset($this->properties[$name])) {
            unset($this->properties[$name]);
        }
    }
}

ここでご紹介した以外にも、特定のDIコンテナ要素への getter メソッドや、SQLクエリ生成の補助、バリデーションなど、幅広い要素に trait が活用出来るでしょう。

4. 実装時の小ネタ

私が実際に trait を実装する際、少し気になったネタもご紹介します。

4-a. 継承との併用

もちろん、 trait は継承と併用することも可能です。

<?php
trait SomeTrait
{
    public function someFunc()
    {
        echo "SomeTrait::someFunc\n";
    }
}
abstract class FooBase
{
    use SomeTrait;
}
class Bar extends FooBase
{
    public function someFunc()
    {
        echo "Bar::someFunc\n";
        parent::someFunc();
    }
}

親クラスで利用された trait は、その親クラスに実装されているものとしてアクセス出来ます。

<?php
$bar = new Bar;
$bar->someFunc();

// 以下が出力される
// Bar::someFunc
// SomeTrait::someFunc

継承先クラスで parent で呼んだ時、 FooBase::someFunc メソッドは実装されていませんが、さらに someTrait::someFunc メソッドを見に行ってそれを呼び出します。

但し、 use したクラスで trait のメソッドを上書きすると、 as などを使わないと trait 側のメソッドが使えませんので注意です。

class Foo
{
    use SomeTrait {
        SomeTrait::someFunc as traitFunc;
    }

    public function someFunc()
    {
        // parent::someFunc() では呼べない
        // SomeTrait::someFunc() とも呼べない
        $this->traitFunc() // as で呼べるようになった
    }
}

4-b. trait が状態(プロパティ)を利用する場合

もちろん trait はプロパティ(メンバー変数)を実装することも可能です。
しかし、メソッドと違ってプロパティの名前の衝突は回避させることが出来ません

trait SomeTrait
{
    public $property = 'Some';
}
trait OtherTrait
{
    public $property = 'Other';
}
class TraitUser
{
    use SomeTrait, OtherTrait;
}

// 以下のエラーが実行時出力される
// PHP Fatal error:  SomeTrait and OtherTrait define the same property ($property) in the composition of TraitUser. However, the definition differs and is considered incompatible. Class was composed in /home/capiba-/test.php on line 13

trait と実装クラスでのプロパティ衝突も同様のエラーが発生します。

trait 内のみで使われるプロパティに関しては、かぶらないような prefix をつけるなどで問題無いと思います。

実装クラス側で設定して欲しいプロパティは、 trait 側で宣言してしまうと実装クラス側で再宣言出来ないので、

  • getter メソッドを用意したり
  • ドキュメントを通して const を定義してもらったり

するのがいいかと思います。

trait FooTrait
{
    // trait 名を prefix にして専用プロパティっぽくする
    private $foo_value = 'something';

    // 実装クラス側で変数を設定して欲しい場合 getter を用意
    abstract private function getBarValue();

    public function getSomething()
    {
        // getter から取得
        $bar_value = $this->getBarValue();

        // const を定義してもらって取得
        $class = get_called_class();
        $baz_value = $class::BAZ_VALUE;

        // ...
    }
}

長々と書いてしまいましたが、本日はこれで以上です。この記事が、あなたが trait を使いはじめる際の参考になればと思います。

参考にさせて頂いた記事

今回の記事を作成するにあたり、以下の記事を参考にさせて頂きました。ありがとうございます。

若干内容は異なりますが、社内勉強会で公開したプレゼンの資料もこっそり。

1件のコメント

  1. […] PHP5.4 の新機能 trait のまとめと実際の利用例 | 株式会社インフィニットループ技術ブログ 弊社技術ブログへお越しのみなさま、こんにちは。今年度入社の新人、 capiba- です。 先日、社内 […]

    2014年8月11日 00:01

  • このブログについて

    このブログは、札幌市の「株式会社インフィニットループ」が運営する技術ブログです。
    お仕事で使えるITネタを社員たちが発信します!

    最新の記事