Delegateクラス2008年03月11日 03時00分03秒

zf入門も書かずにコネタクラスです。ごめんなさい。今回はZend_Logのサンプルもちょっとだけ載せてるので勘弁してください。Zend_Logは説明してないけど。

string | array が気持ち悪いので

PHPで動的な関数・メソッド呼び出しを行う場合、関数名を示す文字列を使うか、配列にオブジェクト(or クラス名)とメソッド名を格納して渡す。マニュアル中で「callback」という呼称で呼ばれている擬似的な型なやつだ。

これ、短いスコープで使う場合はまぁそれでもいいかと思えるんだけど、大きいスコープの変数に格納した場合、本当に適切かどうかの判断がすぐにつかなくて気持ち悪い。$callbackとかって変数にint放り込んでも許容されるので実行時にチェックするのって面倒くさいじゃん。is_callback()とかあるわけじゃなし。

だもんで、.NETのDelegateのような感じで、あらかじめラッピングしちゃえば、関数やメソッドが実在するかはともかくとして「呼び出し可能なオブジェクト」として保持しておけるだろうと思って、こんなクラスを作ってみた。まんま「Delegate」というクラス。使いどこ、少なそうなんですが

<?php
class Delegate {
  /**
   * @static
   *
   * 指定の引数がコールバック形式かを判別する
   *
   * @param mixed $callback コールバック形式かを判別する引数
   * @return bool $callbackがコールバック形式の場合はtrue、それ以外はfalse
   */
  public static function isCallback($callback) {
    // 文字列か配列のみ許容
    if( is_string($callback) || is_array( $callback ) ) {
      if( is_array($callback) ) {
        if(
          // 配列長が2ではないか
          count($callback) != 2 ||
          // 第一要素がオブジェクトまたは文字列ではないか
          ( ! is_object($callback[0]) && ! is_string($callback[0]) ) ||
          // 第二要素が文字列ではない場合は
          ! is_string($callback[1])
        ) {
          // コールバックではない
          return false;
        }
      }
      return true;
    } else {
      // 文字列・配列以外はコールバックではない
      return false;
    }
  }

  /**
   * @access protected
   *
   * コールバックオブジェクト
   *
   * @var string|array
   */
  protected $_callback;

  /**
   * Delegateの新しいインスタンスを初期化する
   *
   * @param mixed $obj コールバック関数名、クラス名またはオブジェクトインスタンス
   * @param  string|null $method $objがクラス名かオブジェクトインスタンスの場合はメソッド名
   */
  public function __construct($obj, $method = null) {
    $callback = ( is_string($obj) && empty($method) ) ?
      // $objが文字列で$methodがnullの場合は関数名指定
      $obj :
      // それ以外はメソッド指定
      array( $obj, "$method" );

    // コールバック形式かをチェック
    if( ! self::isCallback($callback) ) {
      throw new Exception( '引数がコールバック形式ではありません' );
    }

    $this->_callback = $callback;
  }

  /**
   * コールバック呼び出しを実行する
   *
   * @param [mixed parameter [, mixed ...]] 可変長パラメータ
   * @return mixed コールバックの実行結果
   */
  public function invoke() {
    return $this->invokeArray(func_get_args());
  }

  /**
   * パラメータを配列で指定してコールバック呼び出しを実行する
   *
   * @param array $params コールバック関数・メソッドに指定する引数の配列
   * @return mixed コールバックの実行結果
   */
  public function invokeArray(array $params) {
    return call_user_func_array($this->_callback, $params);
  }
}


ま、元の発想のまま「Delegate」なんて名前にしてるけど、別に「Callback」クラスとか「MethodInvoker」とかでもいいです。

ドキュメントコメントが適当なのは勘弁してください。ちゃんと調べよう、そのうち。

「Delegate」とはいいつつも

当然ながら、.NETのDelegateクラスとはちみっと違う。まぁ、あちらは明示的にDelegate/MulticastDelegateから派生型作れるわけではなく、構文からコンパイラが勝手に派生させるっていう特殊な型なんだけども。

コールバックの引数でバインドするわけではないので、invoke()するときの引数に関してなんの保証もないので、ランタイムエラーになりやすいかも。

使い方

コンストラクタの引数に、「callback」型を指定する。

例えば関数の場合ならその関数名の文字列。クラスメソッド(スタティックメソッド)だったら「array( 'ClassName', 'methodName' )」のように文字列を2つ、インスタンスメソッドなら「array( $obj, 'methodName' )」みたいな感じ。

んで、格納したコールバックを呼び出す場合はinvoke()メソッドを使う。引数は可変長で受け取るので、元の関数(またはメソッド)に与える引数と同じような引数を与えてやる。元の関数(メソッド)が値を返すならinvoke()の戻りで受け取れる。

サンプル

以下はZend_Logの各種ログ出力メソッドをDelegateに収めてループで呼び出しするサンプル。

<?php
require_once 'Delegate.php';
require_once 'Zend/Log.php';
require_once 'Zend/Log/Writer/Stream.php';

$log = new Zend_Log( new Zend_Log_Writer_Stream('php://output') );

$list = array(
  // Zend_Log::emerg()を実行するDelegate
  array( 'name' => 'EMERG', 'callback' => new Delegate( $log, 'emerg' ) ),

  // Zend_Log::err()を実行するDelegate
  array( 'name' => 'ERR',   'callback' => new Delegate( $log, 'err' ) ),

  // Zend_Log::warn()を実行するDelegate
  array( 'name' => 'WARN',  'callback' => new Delegate( $log, 'warn' ) ),

  // Zend_Log::debug()を実行するDelegate
  array( 'name' => 'DEGUB', 'callback' => new Delegate( $log, 'debug' ) ),

);
// フィルタ設定なしで実行 → すべてのログが出力される
foreach( $list as $config ) {
  $callback = $config['callback'];
  $callback->invoke( "name = {$config['name']}" );
}

// フィルタ条件を変えてもう一度ループ実行
// フィルタでZend_Log::ERR以下のプライオリティをブロックする
echo "Zend_Log::ERRでフィルタ指定\n";
$log->addFilter( new Zend_Log_Filter_Priority( Zend_Log::ERR ) );
foreach( $list as $config ) {
  $callback = $config['callback'];
  $callback->invoke( "name = {$config['name']}" );
}


このサンプルではDelegateのリストをベタでコーディングしちゃったけど、Zend_Logのプライオリティ名の配列を元にループ処理で組み立ててもよいかも。

しかし、微妙か。

こんな感じで使うんだけど、微妙。独自クラスにしちゃってるので当然array_filter()とかのコールバックにはそのまま使えないし($_callbackを返すgetterでも実装すればいいのだが)、コンストラクタで引数の型はチェックしているけど、別に関数・メソッドの実在をチェックしてるわけじゃないので、ランタイムでエラーになるかもしれないし。第一、ちょっとした用途なら素直にcall_user_func()とか使ったほうが手っ取り早いし

それでも個人的には上のサンプルみたいに同じ形のメソッドを呼び出すちょっとしたテストに使ったりとかでそれなりに便利には使ってるんだけどね。

多様的にメソッド呼び出し行うなら、インターフェイス(か抽象クラス)から派生・実装させるのが本筋なんだろうけど、これなら組み込み関数も自作クラスのインスタンスメソッドもシグニチャが同じものなら同じように代替させられるので、その点だけはメリットかな、と思うんだけど。でもやっぱりcall_user_func()するのがPHPの流儀かしら。