PHPで使うGoFパターン ひとり Advent Calendar - インタプリタ


インタプリタってなに?
---
通訳ってことですね。身も蓋もない言い方をすれば目的特化言語を作って処理を簡略化しましょう!っていう出来るんならやってみたいわボケぇっていうパターンです。SQL文や正規表現、元々のPHPの出自についてもこのパターンだと言えばそうかも知れないです。そんな大仰なものでなくても、例えばフリーワード検索でANDとORと括弧を解釈する時なんかにも使えますね。ただ、これをクラス構造の設計パターンと呼べるのかどうかは謎です。

インタプリタの構造
---
これも構造どうこう言うよりも見た方が早いかも知れないですね。
っていうか、構造はコンポジットと一緒です。

よくあるフリーワード検索の文法を簡易にしてSQL文に変換する仕組みを作ってみました。これは 右、オペレータ、左 にしてオブジェクト化するので、オペレータのメソッドに右、左を引数で渡して計算するようにしてみたりすると、なんとなくDSLっぽいものが出来るイメージが出来たでしょう? 新しいオペレータを追加したい時には、IQueryのインターフェースを継承すれば、追加できます。今回は理解し易くするためにオペレータのインスタンス生成を直書きしましたが、ファクトリを作ってプラガブルにしてみたりするともっと扱い易くなります。


```
<?php
class QueryParser {
  protected $_Arguments = array ();
  protected $_Query     = null;
  protected $_Objects   = null;

  public function __construct ( $q ) {
    $this->_Query = $q;
    $this->parse();
  }

  public function setArguments ( $args ) {
    $this->_Arguments = $args;
  }

  public function parse ( $query = null ) {
    if ( !$query ) $query = $this->_Query;

    $quote = "";
    $length = mb_strlen ( $query, 'UTF8' );
    for ( $i = 0; $i < $length; $i++ ) {
      $char = mb_substr ( $query, $i, 1, 'UTF8' );
      if ( $quote ) {
        if ( $quote == $char ) {
          $this->_Arguments[] = $buf; 
          $quote = null;
          $buf   = null;
        }
        elseif ( $char == '\\' ) {
          $buf .= mb_substr ( $query, ++$i, 1, 'UTF8' );
        }
        else {
          $buf .= $char;
        }
      }
      elseif ( $char == '"' || $char == "'" ) {
        if ( $buf ) $this->_Arguments[] = $buf;
        $quote = $char;
        $buf   = null; 
      }
      elseif ( $char == ' ' ) {
        if ( $buf  ) {
          $this->_Arguments[] = $buf;
          $buf = null;
        }
      }
      else {
        $buf .= $char;
      }
    }
    if ( $buf ) $this->_Arguments[] = $buf;
    return $this->_Arguments;
  }

  public function getArguments () {
    return $this->_Arguments; 
  }

  public function createQuery ( $idx = 0 ) {
    if ( !$idx && $this->_Arguments[0] != '(' ) {
      array_unshift ( $this->_Arguments, '(' );
      $this->_Arguments[] = ')';
    }

    $group = new QueryGroup();
    for ( $i = $idx; $i < count ( $this->_Arguments ); $i++ ) {
      if ( $this->_Arguments[$i] == '(' ) {
        list ( $obj, $i ) = $this->createQuery ( $i + 1 );
        $group->set ( $obj );
        $i++;
      }
      elseif ( $this->_Arguments[$i] == '&&' ) {
        $group->set ( new QueryAnd ( $this->_Arguments[$i] ) );
      }
      elseif ( $this->_Arguments[$i] == '||' ) {
        $group->set ( new QueryOR ( $this->_Arguments[$i] ) );
      }
      elseif ( $this->_Arguments[$i] == ')' ) {
        break;
      }
      else  {
        $group->set ( new QueryOperand ( $this->_Arguments[$i] ) );
      }
      if ( $group->isFull () ) break;
    } 

    if ( $idx ) return array ( $group, $i );
    else        return $group;

  }
}

interface IQuery {
  public function toQuery();
}

class QueryAnd implements IQuery {
  public function __construct ( $e ) {
    $this->_Element = $e;
  }
  public function toQuery () {
    return 'AND';
  }
}

class QueryOR implements IQuery {
  public function __construct ( $e ) {
    $this->_Element = $e;
  }
  public function toQuery () {
    return 'OR';
  }
}

class QueryOperand implements IQuery {
  private $_Element = null;
  public function __construct ( $e ) {
    $this->_Element = $e;
  }

  public function toQuery () {
    return "idx LIKE '%" . $this->escape () . "%'";
  }

  public function escape () {
    return str_replace ( "'", "''" , $this->_Element );
  }
}

class QueryGroup implements IQuery {
  protected $_Left     = null;
  protected $_Operator = null;
  protected $_Right    = null;
  public function set ( $obj ) {
    if     ( !$this->_Left )     $this->_Left     = $obj;
    elseif ( !$this->_Operator ) $this->_Operator = $obj;
    elseif ( !$this->_Right )    $this->_Right    = $obj;
  }

  public function isFull () {
     return $this->_Left &&
       $this->_Operator &&
       $this->_Right;
  }

  public function toQuery () {
    $result = '';
    if ( $this->_Left )     $result .= $this->_Left->toQuery() . ' ';
    if ( $this->_Operator ) $result .= $this->_Operator->toQuery() . ' ';
    if ( $this->_Right )    $result .= $this->_Right->toQuery() . ' ';
    if ( $result ) return '(' . trim ( $result ) . ')';
    else           return '';
  }
} 

function say ( $s )  { print "$s\n"; }
$obj  = new QueryParser ( 
  'hoge        ||        ("f uga " && "pi\\"yo")' 
);
$args = $obj->getArguments();
say ( array_shift ( $args ) == 'hoge'   ? "OK" : "NG" );
say ( array_shift ( $args ) == '||'     ? "OK" : "NG" );
say ( array_shift ( $args ) == '('      ? "OK" : "NG" );
say ( array_shift ( $args ) == 'f uga ' ? "OK" : "NG" );
say ( array_shift ( $args ) == '&&'     ? "OK" : "NG" );
say ( array_shift ( $args ) == 'pi"yo'  ? "OK" : "NG" );
say ( array_shift ( $args ) == ')'      ? "OK" : "NG" );
say ( $args ? 'NG' : 'OK' );
say ( $obj->createQuery()->toQuery() == "((idx LIKE '%hoge%' OR (idx LIKE '%f uga %' AND idx LIKE '%pi\"yo%')))" ? 'OK' : 'NG');
```