LionHeart SD BLOG

株式会社ライオンハート システムデザインの技術ブログ

PHPで多重継承をする(PHP5.4以降 trait利用)

こんにちは、株式会社ライオンハートの鵜飼です。

コチラのブログも書いてくれる人が増えてきたので賑やかになってきました。最初に書かせてもらっていた自分としては嬉しい限りです。細々と続けていますが、誰かの目に止まり、その方にとって役に立つ情報となっていれば幸いです。

それでは今回ですが、また技術寄りの話になりまして、PHP5.4で追加されていたようなのですが、最近まで知らなかったtraitという機能についての内容です。ちゃんと最新情報をおってないとダメですね…。

継承について

同じような機能を持つオブジェクトが沢山ある時に、継承を利用すると非常に便利なのですが、2つのオブジェクトを継承しようとすると、PHPのextendsは1つしか指定することが出来ません。

<?php

abstract class ClassA
{
    public function hoge()
    {
        echo 'HOGE';
    }
}

abstract class ClassB
{
    public function fuga()
    {
        echo 'FUGA';
    }
}

class ClassAB extends ClassA, ClassB // <- こう書きたいけどエラーが発生する
{
}

interfaceでは宣言しか出来ないですし、どうにか2つのオブジェクトの処理を継承させたい…こういった場合に、traitを利用すると解決することが出来ます。(厳密には通常の継承とは異なりますが…)

traitを利用する

一見に如かず、ということで、実際に記述した場合はこのようになります。

<?php

trait ClassA
{
    public function hoge()
    {
        echo 'HOGE';
    }
}

trait ClassB
{
    public function fuga()
    {
        echo 'FUGA';
    }
}

class ClassAB
{
    use ClassA, ClassB;
}

$classAB = new ClassAB;
$classAB->hoge();
$classAB->fuga();

または、extendsと併用することも可能です。

<?php

abstract class ClassA
{
    public function hoge()
    {
        echo 'HOGE';
    }
}

trait ClassB
{
    public function fuga()
    {
        echo 'FUGA';
    }
}

class ClassAB extends ClassA
{
    use ClassB;
}

$classAB = new ClassAB;
$classAB->hoge();
$classAB->fuga();

優先順位について

extendstrait

extendstraitで継承したオブジェクトに、同じメソッドが存在した場合、traitで継承したメソッドが優先され、parentextendsのメソッドを呼び出すことが可能です。

<?php

abstract class ClassA
{
    public function sameFunction()
    {
        echo 'foo';
    }
}

trait ClassB
{
    public function sameFunction()
    {
        parent::sameFunction();
        echo 'bar';
    }
}

class ClassAB extends ClassA
{
    use ClassB;
}

$classAB = new ClassAB;
$classAB->sameFunction(); // 'foobar'

traitと継承先オブジェクト

traitで宣言したメソッドを継承先オブジェクトでも宣言した場合、継承先オブジェクトのメソッドが優先されます。ただし、一度上書きした場合extendsとは異なり、traitで宣言したメソッドを呼び出すことはできません。

この辺り、厳密には継承ではないところが分かりますね。

<?php

trait TraitClass
{
    public function sameFunction()
    {
        echo 'foo';
    }
}

class ChildClass
{
    use TraitClass;
    public function sameFunction()
    {
        echo 'bar';
    }
}

$childClass = new ChildClass;
$childClass->sameFunction(); // 'bar'

先ほどのextendsとの優先順位とコチラの処理の流れを見ていると、継承とは異なることがわかると思います。個人的にはtraitについては「テンプレート」みたいな印象を持っています。

f:id:lh-sukai:20160617203238p:plain

useと記述しているところに実際に記述をしてあるような印象です、伝わりますでしょうか…)

衝突と解決

上述のような、extendstraitと継承先のメソッドは同じ名称で記述しても、優先順位だけ気をつければ問題ありませんが、trait同士で同じメソッドを記述してしまうとfatalエラーが発生してしまいます。

<?php

trait ClassA
{
    public function sameFunction()
    {
        echo 'HOGE';
    }
}

trait ClassB
{
    public function sameFunction()
    {
        echo 'FUGA';
    }
}

class ClassAB
{
    use ClassA, ClassB; // fatalエラー
}

こういった場合には、insteadof演算子を利用してどちらを優先するか明示することで解決することが可能です。

また、insteadofだけではどちらか一方のメソッドしか利用することが出来ませんが、as演算子を利用して別名に変更することで、双方のメソッドを利用することも可能です。

下記例の場合、functionAはClassAを、functionBはClassBを優先し、ClassBのfunctionAはdifferentFunctionAという名称で宣言しています。

<?php

trait ClassA
{
    public function sameFunctionA()
    {
        echo 'ClassA function A';
    }
    public function sameFunctionB()
    {
        echo 'ClassA function B';
    }
}

trait ClassB
{
    public function sameFunctionA()
    {
        echo 'ClassB function A';
    }
    public function sameFunctionB()
    {
        echo 'ClassB function B';
    }
}

class ClassAB
{
    use ClassA, ClassB {
        ClassA::sameFunctionA insteadof ClassB;
        ClassB::sameFunctionB insteadof ClassA;
        ClassB::sameFunctionA as differentFunctionA;
    }
}

$classAB = new ClassAB;
$classAB->sameFunctionA(); // 'ClassA function A'
$classAB->sameFunctionB(); // 'ClassB function B'
$classAB->differentFunctionA(); // 'ClassB function A'

ややこしいですが、柔軟に対応が可能そうな印象ですね。

雑感

個人的には、既存のフレームワークを利用する際に、もう少しこうしたいけど、多重継承するのも微妙…と言った場合に、フレームワークのクラスを継承しつつ、自分で作ったtraitを追加する、といった形で利用する際に重宝しています。

その他、静的メソッドやプロパティも宣言出来たり、抽象化も出来たりするので、知っておいて便利な機能だと感じました。

あまり追加された機能について追いかけれていませんでしたが、他にも見落としている機能がありそうなきがしてきました。再度見なおしてみようと思います。

参考


おまけ

文中で、上書きしたtraitのメソッドを呼び出すことは出来ない、と記述しましたが、強引に実現することは可能です。

強引に上書きしたtraitのメソッドを呼び出す

useの際にメソッドのエイリアスを生成することが可能なので、一旦traitのメソッドを逃がしておき、上書きしたメソッド内からそのメソッドを呼び出すことで、擬似的なparentメソッドを実現することが可能です。

<?php

trait TraitClass
{
    public function sameFunction()
    {
        echo 'foo';
    }
}

class ChildClass
{
    use TraitClass {
        // 一旦traitのメソッドを逃がしておく
        sameFunction as traitFunction;
    }

    public function sameFunction()
    {
        // 逃がしておいたメソッドを実行する
        $this->traitFunction();
        echo 'bar';
    }
}

$childClass = new ChildClass;
$childClass->sameFunction(); // 'foobar'

ただ、本来の使い方から離れてしまっていることと、プログラム自体が読みづらくなってしまうことを考えると、利用するのは控えたほうが良いでしょう。