2012年5月31日木曜日

beforeFind、afterFindで特定のフィールド全てを加工する

熱とかでダウンしてた為久々のエントリ。今、beforeFindの練習も兼ねて郵便番号が送られたら、小数(decimal型)に加工ということをbeforeFind時に行ってるんだけど、前回のコードだと、全ての郵便番号フィールド(zipcode)を加工してくれず、ただzipcodeと送られた時だけにしか働いてくれない。

ちなみに、前回のコードはこんな感じ。

//Model/PostalCode.php内
   public function beforeFind($queryData){
      if(!empty($queryData['conditions']['zipcode'])
         && preg_match("/^(\d{3})\-?(\d{0,4})$/i",$queryData['conditions']['zipcode'],$match)){
         $zip_len = strlen($zipcode = (int)($match[1].$match[2]));
         if($zip_len < 7){ //数値が7桁未満の場合
            $from_zipcode = (int)($zipcode . "0") / pow(10 , $zip_len + 1);
            $to_zipcode = (int)($zipcode . "9") / pow(10 , $zip_len + 1);
            $queryData['conditions']['zipcode BETWEEN ? AND ?'] = array($from_zipcode   , $to_zipcode);
            unset($queryData['conditions']['zipcode']);
         }else{//それ以外
            $queryData['conditions']['zipcode'] = (int)($match[1].$match[2]) / 10000000;
         }
      }
   }

この場合、find時に単純に'conditons'=>array('zipcode'=>'郵便番号')と送った時はbeforeFindさんが仕事をしてくれる。
でも、cakePHPのfindでは色んな形でこのzipcodeを送る機会がでてくる。

たとえばIN句で送りたい場合、'zipcode'=>array('郵便番号1','郵便番号2',...)と送ることになるようで、そういう場合、上記のだとエラーが起きちゃうと思う。というか、さっき試してみたらやっぱり起きた(._.)最近は公務員だってもうちょっと融通きいてくれるけど、こいつは完全に「zipcode=文字列」しか受け付けない。

そんなわけで、今回はどんなパターンで送ってもちゃんと仕事をしてくれるように作り替えてみようと思います。

検索の仕方のパターンを調べる

とりあえず、サブクエリはcookbookだけを見てもどんなデータの形になるのか全然わからないんで、今回は無視。以下は全部今回のPostalCodeモデルの郵便番号(zipcode)を検索する例として記述する。

<テーブル構成はこちら>

モデル名を指定する場合と指定しない場合がある

通常の場合、zipcodeで検索する場合以下の形でできる。

$this->PostalCode->find('all',
    array(
       'conditions'=>array(
          'zipcode'=>'103-0003'
        )
    )
);

ただ、hasManyなどで複数テーブルをjoinする場合、きちんとテーブル名(モデル名)を指定しないとエラーが起きることがある(と思う)。
その場合は以下のように記載する。(以下、途中は省略します)

'conditions'=>array(
  'PostalCode.zipcode'=>'103-0003'
)
IN句や範囲検索、OR句での検索の場合

上記の場合、SQLではzipcode = 郵便番号として発行される。
等号としてじゃなく、範囲検索などを使いたい場合は以下のようになる。

■IN句を使う場合──値を配列で記載する。

'zipcode'=>array(
   '郵便番号1','郵便番号2',... 
)

(発行) zipcode in (郵便番号1,郵便番号2,...)

■NOT IN句を使う場合──NOTキーで値を配列で記載する。

'NOT'=>array(
   'zipcode' => array('郵便番号1','郵便番号2',... )
)

(発行) zipcode NOT in (郵便番号1,郵便番号2,...)

■OR句を使う場合──"OR"キーでの配列で記載

'OR'=>array(
   'zipcode'=>'郵便番号',
   '他のフィールド'=>'他の条件'
)

(発行) ((zipcode = 郵便番号) OR (他のフィールド = 他の条件))

■> , < , >= , <= , !=での検索──キーの最後にそれらを記載

'zipcode >' => '郵便番号'
(発行) zipcode > 郵便番号

■Betweenでの検索──「キー名 BETWEEN ? AND ?」と記載

'zipcode BETWEEN ? AND ?' => array(値1,値2)
(発行) zipcode BETWEEN 値1 AND 値2

要は、フィールド名のキーの形は検索の仕方でいろいろ変わっちゃったり、前についたり後ろについたり、配列の中に入り込んだり、値も配列だったり色々とするのね。
これをひとつの関数で全部補うというのは面倒だなぁ。

再帰関数を使って実装してみる

うまくいくかわからないけど、とりあえず全てのキーを再帰関数で読み取り、特定のフィールド名が存在したら値を操作するというものを作成してみた。
ぼくの場合、datetime型をintで管理する、ということをよくするし、色々と使いそうだからAppModel内に作成する。

//AppModel内
//特定のフィールドをキーごと加工する
public function changeField(&$queryData , $fieldName = null , $callback = null , $isarray = false){
  if(empty($callback) || empty($fieldName)) return;
  if(is_array($queryData)){
     foreach($queryData as $key => $rows){
        if( 
          preg_match('/'.implode('|',array_map(
             function($a){ 
                return  strtr($a, array("-"=>"\-","."=>"\.","?"=>"\?")) . '$';
             },(array)$fieldName)).'/i',$key , $match)
          && 
          ($isarray || !is_array($rows)))
        {
           if(is_object($callback)){
              $callback(&$queryData[$key] , $key);
           }else{
              if(is_array($callback)){
                 $_callback = array_shift($callback);
                 $arg = array_values($callback);
              }else{
                 $arg = array();
                 $_callback = $callback;
              }
              array_unshift($arg , &$queryData , $key);
              call_user_func_array(array($this , $_callback) , $arg);
           }
        }else{
           self::changeField(&$queryData[$key] , $fieldName , $callback , $isarray);
        }
     }
  }
}
//AppModel内
//特定のフィールドの値全てを加工する
public function changeValue($queryData , $fieldName = null , $callback = null , $recursive = 0, $current = array()){
   if(empty($callback)) return $queryData;
   if(is_array($queryData)){
     $ary = array();
     foreach($queryData as $key => $rows){
       $_current = $current;
       array_push($_current , $key);
       if($recursive > 0 && count($_current) > $recursive) array_shift($_current);
       $ary[$key] = self::changeValue($rows , $fieldName , $callback  , $recursive , $_current);
     }
     return $ary;
   }else{
     if(preg_match("/(".strtr($fieldName , array("-"=>"\-","."=>"\.","?"=>"\?"))."[^\n]*)/i" , implode("\n" , $current),$match)){
       $key = $match[1];
       if(is_object($callback)){
          return $callback($queryData , $key);
       }
       if(is_array($callback)){
          $_callback = array_shift($callback);
          $arg = array_values($callback);
       }else{
          $arg = array();
          $_callback = $callback;
       }
       array_unshift($arg , $queryData , $key);
       return call_user_func_array(array($this , $_callback) , $arg);
     }else{
       return $queryData;
     }
   }
}

多分、基本的に値を変更するだけなら全部changeValueだけでできると思う。ただ、今回ぼくの郵便番号検索の作り方だと値によってはBETWEENに変更したりという(面倒な)ことをするんで、そういう場合はchangeFieldを使う。
使い方は以下の通り。

changeValue(クエリデータ、検索フィールド名、コールバック、階層(どこまで追うか))
changeField(クエリデータ、フィールド名、コールバック、フィールドの値が配列の場合も適応するか)

コールバックは以下のように指定
・文字列(関数名)
・配列(array(関数名,引数1,引数2,...))
・無名関数

コールバック関数(その値、キー、他引数)

ちなみに、キーの名前に改行が入ってたりすると失敗する仕様です。
コールバックは、無名関数でもモデル内の関数でもできるようにしてみた。
これでできるといいんだけど。

前回のを書き直してみる
//PostalCode.php内
 public function beforeFind($queryData){
     $this->changeField($queryData['conditions'] , array('zipcode' , 'zipcode like') , array('setZiptoBetween','zipcode'));
     $queryData = $this->changeValue($queryData , 'zipcode' , 'changeZipCode');
     return $queryData;
 }
 
 protected function setZiptoBetween(&$queryData , $key , $field){
   if(preg_match('/(\d{3})-?(\d{0,4})/' , $queryData[$key] , $match)){
     if(strlen($val = $match[1].$match[2]) < 7){
        unset($queryData[$key]);
        $queryData[$this->alias . '.' . $field . ' BETWEEN ? AND ?'] = array($val.'0',$val.'9');
     }
   }
 }
 
 protected function changeZipCode($data){
   if(preg_match('/(\d{3})-?(\d{0,4})/',$data , $match)){
       $data = (int)$match[1].$match[2];
       return $data / pow(10 , strlen($match[1].$match[2]));
   }else{
       return $data;
   }
 }

 public function afterFind($result){
   return $this->changeValue($result , 'zipcode' , 
     function($data){
       if(preg_match("/^(\d{3})(\d{4})$/i",str_pad($data * 10000000 , 7 , '0' , STR_PAD_LEFT), $match)){
          return $match[1] . '-' . $match[2];
       }else{
          return $data;
       }
     }
   );
 }

できたっぽい…けど、もうちょっとスマートなやり方もある気がするなぁ。
せめて、2つの関数を1つにまとめたいけど、ぼくの実力じゃこれが限界ですた。

というより、こういう関数はcakePHP内で実装されてそうな気もするんだけど、そもそも値を事前に加工するというようなことはしないものなのかな?

0 件のコメント:

コメントを投稿