phpフレームワークで柔軟な検索項目条件の絞り込みを実現する方法

昔phpフレームワークを勉強している時思いついた実装方法。
とりあえずFuelPHPで書いているけど他のフレームワークでも似たような事ができるはず。

#想定する対象の画面・機能

管理画面などでよくある、「複数の検索条件を任意にANDで繋げて絞り込みを行う」という機能。
例えばユーザのデータであれば

- メールアドレスと名前の検索フォームがある
- メールアドレスの欄にワードを入れればメールアドレスに、名前の欄にワードを入れれば名前に対する検索が実行される
- 両方にワードを入れれば双方に一致するAND検索となる

という風な挙動を想定。

#想定する検索のパターン

- statusは完全一致する（公開、非公開など、ステータスをselectで選択する事をイメージ）
- name, descriptionは部分一致する（入力されたキーワードと部分一致するかテキスト検索するイメージ）
- created_atは日付の範囲内に含まれるか（登録日時が範囲指定内にあるか検索するイメージ）

#実装の手順

まず、Modelクラスに検索の条件の情報を入力する。
これにより絞り込みを行うメソッドは`get_by_params()`。

```php:classes/model/product.php
class Model_Product extends Model_Abstract
{
    protected static $_search_cond = array(
        'status' => array('field' => 'status', 'pattern' => 'equal'),
        'type' => array('field' => 'name', 'pattern' => 'like'),
        'name' => array('field' => 'description', 'pattern' => 'like'),
        'date_from' => array('field' => 'created_at', 'pattern' => 'date_larger'),
        'date_to' => array('field' => 'created_at', 'pattern' => 'date_smaller'),
    );

    public static function get_by_params($params){
        $query = self::query();
        $query = self::add_query_params($params);
        return $query->get();
    }
}
```

`Model_Abstract `には次のような形で`add_query_params()`を実装する。

```php:classes/model/abstract.php
class Model_Abstract extends \Orm\Model_Soft
{
    public static function add_query_params($query, $params){
        if(!empty($params)){
            $search_cond = static::$_search_cond;
            foreach ($params as $key => $param){
                if(empty($search_cond[$key])){
                    continue;
                }
                $field = $search_cond[$key]['field'];
                $pattern = $search_cond[$key]['pattern'];
                switch ($pattern){
                    case 'like':
                        // 半角スペースで区切られている場合、そのワードの分だけ分割してAND検索する。
                        $param_splits = explode(' ', $param);
                        foreach ($param_splits as $param_split){
                            if($param_split == ''){
                                continue;
                            }
                            // LIKEのワイルドカードを入力されていた場合のためエスケープ
                            $string = '%' . str_replace(array('%', '_'), array('\%', '\_'), $param_split) . '%';
                            $query->where($field, 'LIKE', $string);
                        }
                        break;
                    case 'equal':
                        // 「未指定」を'0'で表す事を想定。この方法だと0の完全一致は動作しなくなるので、例外となる文字は決めておく
                        if($param == '0'){
                            continue;
                        }
                        $query->where($field, $param);
                        break;
                    case 'date_larger':
                        $query->where($field, '>=', $param);
                        break;
                    case 'date_smaller':
                        // これは、「YYYY-mm-ddまで」と指定した時、「YYYY-mm-dd 23:59:59」までを検索範囲に含めるための措置
                        $date = date('Y-m-d', strtotime($param . ' + 1 Day'));
                        $query->where($field, '<', $date);
                        break;
                    // この他、必要に応じてINなどのパターンを追加する事も出来る。
                    default:
                        break;
                }
            }
        }
        return $query;
    }
}
```

コントローラから次のように呼び出す。
これで、POSTパラメータに応じて自動的に絞り込み検索が実行される。

```php:classes/controller/product.php
class Controller_Product extends Controller_Application
{
    public function action_index(){
        $product_data = Model_Item::get_by_params(\Input::post());
    }
}
```

#メリット

- 原則として絞り込みの処理条件をクラス内のパラメータ記述だけで制御できる。
- 検索条件を追加する時は`$_search_cond`の変更だけで済むし、他のModelクラスファイルでも同じ書き方で同様の絞り込みが実現できる。