PHP
Laravel
laravel5.5
Macroable

Laravelのtrait Macroableを使って動的にメソッドを追加する


Macroableとは

Laravelをインストールしたときに一緒にインストールされるtraitです。

vendor\laravel\framework\src\Illuminate\Support\Traits\Macroable.phpにあります。

Macroableをuseするとそのクラスは定義されたクラス以外でも動的にメソッドを追加できるようになります。

ソースそのものは108行しかないのでシンプルです。

以下メソッド

public static function macro($name, $macro)

public static function mixin($mixin)
public static function hasMacro($name)
public static function __callStatic($method, $parameters)
public function __call($method, $parameters)

仕組みとしてはmacro()でメソッド名と実行する内容を登録して、マジックメソッドの __call() か __callStatic() で使えるようにするといったものです。

hasMacro()は名前の通りmacro()で登録してあるかチェックするためのメソッドです。

mixin()は5.5から追加されました。名前で推測できるように引数にインスタンスを渡して、そのインスタンスのメソッドを使えるようにするためのものと判断出来ます。

ですが、癖が強いので使い勝手が良いかどうかは疑問です。後述します。


使い方

class Human{

use \Illuminate\Support\Traits\Macroable;
public function punch(){
echo "パンチ";
}
}
Human::macro('kick', function(){
echo "キック";
});
$human = new Human();
$human->punch();
$human->kick();

//結果 パンチキック

Laravelで使うときはProviderで登録するのがまとまりがあって良いでしょう。


mixin()の使い方

まずはmixinメソッドがどのように定義されているか見てみましょう。

public static function mixin($mixin)

{
$methods = (new ReflectionClass($mixin))->getMethods(
ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
);

foreach ($methods as $method) {
$method->setAccessible(true);
static::macro($method->name, $method->invoke($mixin));
}
}

リフレクションを使い、public,protectedのメソッドの一覧を取得してforeachでmacro()で登録する流れになっています。

ですが見てください。macroメソッドの第二引数の渡し方。

$method->invoke($mixin)

普通にメソッドの呼び出しをしています。

もしも直感的にメソッドを使うなら次のように間違って書いてしまうでしょう。

class Cat{

public function say(){
echo "にゃー";
}
}

Human::macro('kick', function(){
echo "キック";
});
Human::mixin(new Cat());
$human = new Human();

$human->punch();
$human->kick();
//$human->say(); BadMethodCallExceptionが発生する

//結果 にゃーパンチキック

say()を呼び出していないのに にゃー と鳴いてしまってます。

say()を呼び出すとBadMethodCallExceptionになります。

どう解決しましょうか。

答えはシンプルです。

say()メソッドの返り値をクロージャーにします。

class Cat{

public function say(){
return function(){
echo "にゃー";
};
}
}

このように定義すればsay()を呼び出してもBadMethodCallExceptionが発生しません。

結果はパンチキックにゃーの順で呼び出されます。

mixin()は無名クラスを使って登録するのがベターな気がします。

Human::mixin(new class{

public function say(){
return function(){
echo "にゃー";
};
}
});


Macroableの使い所は?

第三者に拡張性のあるクラスを提供したい!といった場合くらいですかね。

自分で率先して使う場面というのはあんまり無いでしょう。

バグの原因になりそうです。

実際の用途としてはLaravelで定義してあるクラスを拡張するのに使用するといったところでしょうか。


余談

日付操作ライブラリのCarbonにも同じようなMacroableのようなコードがあります。

個人的にマジックメソッドの __call() や __callStatic() はソースが追いにくくなるため苦手です。