2007-02-15
eval使わずにPHP5でmix-inを実現 (Mixins in PHP without eval)
追記:修正しました
今までで見る限り一番シンプルだったmix-inの方法これでした。
Achievo.org - Achievo Blog(english)
<?php class Module{ function show(){ echo $this->val; } } class Hoge extends Object{ var $mixins = array('Module'); var $val = 'hogehoge'; } $c = new Hoge(); $c->show(); ?>
簡単に仕組みを説明すると
- includeしたいクラス(Hoge)に、includeされる方のクラス(Module)名を指定($mixinsのとこ)
- Object#__construct()でクラス名からメソッド名を取得→インデックス化
- Hogeに不明なメソッドのアクセスが来たらマジックメソッドObject#__call()が呼ばれる
- Object#__call内部では、eval()でメソッド名から適切なクラスを選択して実行する
という流れ。肝心なのはクラス内で他クラスのメソッドを静的メソッドとして呼ぶと他クラスのメソッドを自分クラスのメソッドであるかのように呼べること。
でもなぜかこのshow()にstaticをつけると
> Fatal error: Using $this when not in object context in ~~
こんなエラーが出てしまう。このへんの解釈はよくわかりません。
また、内部でeval()を使ってるのがスマートではないため、先の記事では高度なリフレクションを(invoke)使って解決しようとしてる。けどこのstaticの解釈がよくわからないせいかうまくいかない。
話変わって、先のblogではAutoForwardクラスというおもしろいコードも公開している。
また例を出すと
<?php class Base{ function say(){ echo "yes!"; } } class Bold extends AutoForward{ function say(){ echo '<b>'.$this->m_object->say().'</b>'; } } class Italic extends AutoForward{ function say(){ echo '<i>'.$this->m_object->say().'</i>'; } } $b = new Italic(new Bold(new Base())); $b->say(); // <i><b>yes!</b></i> ?>
という感じでDecoratorパターンが簡単にかけます。これを使えば多重継承もアスペクト指向プログラミングもできるらしいです。内部では__call()や__get()などマジックメソッドを使いまくって、処理を次のクラスに委譲しているわけです(Delegationパターンと言うのだそう)
----
で、ここからが本題。このAutoForwardを使ったらeval()使わずにmix-inが出来るんじゃないかと思って試してみたら…
動きました。
しかも前のeval()でやるより20%ぐらい速い。
HACK : Mixin in PHP5と比較すると50%ぐらい速い。
たぶん、PHP5でのmix-inは今回編み出した方法が今のとこ一番速くてスムーズだと思われます。まぁ、この2つの素晴らしいクラスを作った作者は、たぶんもう気づいてるんだろうな。それにしてもここにたどり着くのに関数の中に関数を定義できることやリフレクションだとかいらん知識もたくさんついたな…。
ではまず、サンプルから。
前のとほぼ同じですが、mix-inとしてincludeされるクラスはAutoForwardクラスの継承がいります。あと、mix-inは継承できないクラスなのでfinalつけときます。abstractにはできないのですが。
<?php final class Module extends AutoForward{ function plus($a, $b){ return $this->base + $a + $b; } } class hoge extends Object{ public $mixins = array('Module'); public $base = 3; } $hoge = new hoge(); echo $hoge->plus(3, 4); // 11 ?>
そして改良版Objectクラス。
<?php class Object { private $_mixinlookup = array(); // The constructor takes a look at the mixins, and creates a lookup // array, so upon a method call, we can quickly determine whether the // method was mixed in. function Object() { if (is_array($this->mixins)) { foreach($this->mixins as $mixin) { $methods = get_class_methods($mixin); if (is_array($methods)) { foreach($methods as $method) $this->_mixinlookup[$method] = new $mixin($this); } } } } // The __call magic method intercepts any method that does not exist // and falls back to one of the mixins if they define the method that is // being called. function __call($method, $args) { if (isset($this->_mixinlookup[$method])) { return call_user_func_array( array($this->_mixinlookup[$method], $method), $args); } trigger_error('Call to undefined function '.$method, E_USER_WARNING); } } ?>
だいぶシンプルになった!マジックメソッドがこんなに使える機能だとは思わなかったよ。__call()や__get()などはワンクッション置くから微妙に遅くなってしまうけど、こういうすっきりしたコードみるのは非常に気持ちがいいな。
