2011年12月6日火曜日

Underscore-Walla-WallaというPHPクラスを作ってみた

初めてアドベントカレンダーに参加したのですが、いきなり内容が被ってしまいました。別のトピックを考えたのですが、思いつかず。まぁ実装が違うので、このままで参加することにしました。別ネタを思いついたので、そちらを書きました。

Underscore.phpという面白いクラスが紹介されてました。

なんでもはunderscoreというjavaScriptをPHPにポートしたものだそうです。何が面白いって、

jQueryみたいにPHPコードが書ける

早速ソースコードを追いかけてみたのですが、すぐ理解できなかったので自分で書き直してみました。なるほど、分かってしまえば、単純なことを丁寧に積み重ねていたんですね。

理解のために書いたコードですが、せっかくなので、衆目の下に晒すことにしました。クラス名は_ww。アンダースコア・ワラワラと読んでください。
ちなみにWalla Wallaはアメリカ合衆国はワシントン州にある小さな町です。昔、あの近くのもっと辺鄙な場所で学生してたことがあったので、名前に使ってみました。

簡単な使い方。

まずは一番簡単なスタティック・メソードで呼ぶ方法。

// use as static method. 
$ww = _ww::each( 
    array( 1,2,3 ), 
    function($n) { echo $n . "\n"; } 
  );
実際のeachの中身は、_wwmというクラス内で定義されています。
class _wwm
{
  static public function each( $collection, $iterator ) {
    if( !empty( $collection ) )
    foreach($collection as $k=>$v) {
      call_user_func($iterator, $v, $k, $collection);
    }
  }
}
$collectionが配列で、foreach文を使って各要素について$iterator関数を呼んでいます。

これだと、普通なPHPといった感じです。
次はメソードチェーンをしてみます。

チェーンで次々と処理する

eachとかをつなぎ合わせてチェーンのように使えます。
// use as a chain of methods. 
_ww::chain(  array( 1,2,3 ) )
  ->each(  function($n) { echo "in chain $n" . "\n"; } )
  ->get(   $mid )
  ->map(   function($n) { return $n * 2; } )
  ->value( $result );
var_dump( $mid );
var_dump( $result );
ポイントは、

  • chainメソードで始めること。
  • valueメソードでチェーンを終了して、結果を受け取ること。
  • チェーンの途中で値を使いたい場合はgetメソードが使うこと。

といった所でしょうか。

チェーンの仕方を理解する

一体どうなっているか、_wwクラスの中を少し見てみます。
class _ww
{
  private $_wrapped = NULL;  // holds the data
  
  public function __construct( $collection ) {
    $this->_wrapped = (array) $collection;
  }
  public static function chain( $collection ) {
    return new _ww( $collection );
  }
}
chainメソード内で、自分をnewしてます。いや、自分自身のインスタンスを作成してます。入力された配列はオブジェクト内の$_wrappedという変数に保存されます。

で、作ったオブジェクトをすぐにreturnしてます。
これでチェーンが始まるというわけです。

メソードの呼び出し方は?

次はeachを呼ぶのですが、_ww自体にはeachなどのメソードは実装されてません。その代わりに「magicメソード」と呼ばれる__callメソードが呼ばれます。
class _ww
{
  public function value( &$result=NULL ) {
    $result = $this->_wrapped;
    return $this->_wrapped;
  }
  public function __call( $method, $args ) {
    $arg  = array_merge( array( $this->_wrapped ), $input );
    $return = call_user_func_array( "_wwm::$method", $arg );
    if( !is_null( $return ) ) {
      $this->_wrapped = $return;
    }
    return $this;
  }
}
この中で、最初に$_wrappedに保存しておいた配列を呼び出して、残りの引数と一緒にして、実際のeachメソードを呼び出しています(call_user_func_array( "_wwm::$method", $arg )の部分)。
ここらへんがShinさんのarryクラスと実装が違うところです。あちらはeachとかが最初からチェーン内で使われることを想定して書かれてます。一方、元ネタのUnderscore.phpは書くメソードがチェーン内にいるかどうかを判定して、引数と返値を制御しています。ちなみに、_wwはスタティックはチェーン外、インスタンスの場合はチェーン内と割り切って、__callマジックメソードで引数などを作り直しています。
メソードの最後にreturn $thisをして次のチェーンへつなぎます。
この後のmapメソードも、同じように処理されます。ただmapだと返り値があるので、$_wrappedの値が変わることになります。

こうして次から次にチェーンにしたがって、$_wrappedに入っている配列に対して処理を行ってゆくことが出来ます。

チェーン部分の出力です

in chain 1       // each内で出力
in chain 2
in chain 3
array(3) {       // $midのvar_dump
  [0]=>  int(1)
  [1]=>  int(2)
  [2]=>  int(3)
}
array(3) {       // $resultのvar_dump
  [0]=>  int(2)
  [1]=>  int(4)
  [2]=>  int(6)
}

最後に

_wwのソースコードはgithubでホスティングしています。
ライセンスはMITライセンスです。

コードをちゃんと見ると、テストコードが入り込んでる、クラスが二つに分かれた、とか問題はたくさんあるのですが、ひとまず動いたので今回はここで完了としました。

よく「車輪の再発明」とか言われますが、こういう小さなクラスを書くのはとても楽しいです。実際に使うとか考えると微妙ですが、細かいことは気にしないで作ってみると楽しいと思います。

0 件のコメント: