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内で実装されてそうな気もするんだけど、そもそも値を事前に加工するというようなことはしないものなのかな?

2012年5月18日金曜日

郵便番号検索を作る─3.郵便番号前方一致で検索

前回までで郵便番号を入力したらその住所が表示されるところまで作った。
本当は住所から郵便番号を検索できるようにするのを目標としてるんだけど、もうちょっと今回のを拡張し、前方一致で検索できるように作り替えてみる。

というより、ある程度手順を先に書いておいた方がいいね。

■作成手順
・郵便番号データを作成
・郵便番号の全文一致でリストを出力
・郵便番号の前方一致でリストを出力←今回ココ
・都道府県、市区町村によるリストを作成
・住所(漢字、カナ、かな)で検索できるように
・郵便番号データに知人/友人を登録できるようにする
・レイアウトをカスタマイズする

こんな感じでいこっかな、と思う。というわけで、作成していきます。

$this->ModelName->findのまとめ

まず、作り出す前にfindについてちょこっとまとめた。この関数は、要はSelectクエリを発行し、cakePHP内で使いやすい配列として返してくれる関数だ。

$this->[ModelName]->find(string $type , array $params);

[ModelName]
モデルの名前をキャメル型で指定する。今回の場合だと$this->PostalCode->find()という形になる。

$type
どういう形で配列を出すか。デフォルトはfirstとなってる。

$typeの種類
first最初の1件だけ配列で返す(limit 1と同じ)
all検索結果を全て配列で返す
count検索の数を返す
listIDと$displayFieldで指定されたものだけ配列で返す*
neighborsデータの前の値と次の値を配列で返す
threadedデータをスレッドの配列型で返す

neighborsとかかなり使えそう。
$displayFieldに関しては、フィールド名にnameまたはtitleがあれば自動的に設定される。
そんなフィールドがない、または別で指定したい場合は以下のようにモデル内で指定する。

//モデル内
class Model extends AppModel{
    public $displayField = 'フィールド名';
}

$params
それぞれ検索条件、フィールド名、ソートなどを指定する。

主な$paramsの指定
conditions検索条件を配列で指定
fields返すフィールドを指定
(指定しなければ全部返す)
orderソートする
groupグループ化する
limit何件返すか指定
offsetlimitからのオフセット値を指定
page検索結果のページを指定
recursive複数テーブルを使用する際、どの階層まで追うかを指定
callbacksbeforeFind、afterFindを使用するか

limitpageの2つを使えば、ページングが容易にできるようになるみたい。詳しい使い方は使う時に調べよ。

conditionsは基本的には配列で'フィールド名'=>'値'という形で指定するが、値を配列にすると自動的にIN句になる。
またBETWEENや >、<、>=、<=を使う時にはフィールド名を特殊な形に変えるみたい。それらも使う時に書いていきます。

コントローラーを変更する

今回作成している郵便番号データは、郵便番号(zipcode)を文字列型ではなくあえて使いづらいdecimal型にしている。
ただ、そのおかげでBETWEEN句で前方一致を実装できる。

■送られた郵便番号が7桁
 →そのまま検索
■送られた郵便番号が6桁以下
 →桁数を計算し、それを元に範囲検索する。
 (例)103-00と送られた → 0.103000 ~ 0.103009を検索

というわけで、まずは前回作成したindex()アクションにて、6桁以下でもaddress()へ渡すように作り変える。

//Controller/PostalCodesController.php内
   public function index(){
     if($this->request->is('post')){
       $data = $this->request->data('PostalCode.zipcode');
       if(!preg_match("/([0-9]{3})\-?([0-9]{0,4})/i" , $data)){
          $this->Session->setFlash("入力が正しくありません。郵便番号は3桁以上の半角数字か***-****形式で入力して下さい");
       }else{
          $this->redirect(array('action'=>'address',$data));
       }
     }
   }

ついでに、前回$data = $this->request->data['PostalCode']['zipcode'];としてた部分を、エラーにならないようにする為に書き換えた。

モデルのbeforeFind()を書き換える

次に、beforeFind()内で郵便番号コードを検索できる形に加工するよう作り替える。

//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;
         }
      }
   }

この'zipcode BETWEEN ? AND ?'というのが、BETWEENでの範囲検索の際に指定する方法となる。

'フィールド名 BETWEEN ? AND ?' = array('範囲1','範囲2');

値が7桁未満なら検索方法をBETWEENに変更し、7桁なら以前と同じ方法で検索する。
ちょっとコードが汚いなぁ。もうちょっと綺麗な作り方もある気がするけど、まあ多分動いてくれるでしょ。

実際に試してみる

さて、この2箇所を変更したら、もう前方一致検索できるようになってるはず。

うんうん、できてる。


というわけで、今回仕様を変更する際、cakePHPなら特定の部分をちょこちょこっと変えることで簡単に変えられることがわかった。
この程度のことなら、普通に作ってもやっぱりちょこちょこっといじることで簡単に変更できそうだけどね。
でも、それぞれの役割や動作がページやポジションによって明確になってることで、どこを変えたらいいかは他の人がいじる場合でもわかりやすそうかも、という印象は持った。
多分それがフレームワークってやつなんだろうね。

【参考リンク】
cakePHP2.x(英語版)──find

2012年5月16日水曜日

郵便番号検索を作る─2.簡単なページを作成

テーブルに関しては前々回に作成&郵便番号データを放り込んだ。ということで、今回からは実際に作成していってみる。

今回やることは以下の通り。

1.コントローラとアクションを作成(index()とaddress()アクション)
2.Formヘルパーを使ってindex.ctpに郵便番号での検索ページを作成
3.モデルのbeforeFind()、afterFind()を使って郵便番号のデータを加工
4.郵便番号は前方一致で検索できるようにする
5.address.ctpにて郵便番号からの住所結果を出力
6.検索結果が存在しない場合はindex()へと戻す


最終的には住所などから郵便番号を出すという機能を実装するつもりだけど、まずは郵便番号を入力したら住所を出す、という機能を作り、その後機能を付け足していくという形で作成していきます。

コントローラーを作成

まずは、どうなるかわからないので空のアクションだけを作成する。

//app/Controller/PostalCodesController
<?php
   class PostalCodesController extends AppController{
        public $helpers = array('Html','Form','Session'); //*
        public function index(){
        
        }
 
        public function address(){

        }
   }
?>
public $helpersの説明はこちら
ビュー(index.ctp)を作成

コントローラーを作成したら、次はビューファイルを作成していく。
ひとまず、index.ctpを作成。ここではFormヘルパーを使って郵便番号入力フォームを作成する。

//View/PostalCodes/index.ctp
<H1>郵便番号検索</H1>
<?php
    //フォームを作成。
    //命名規約に沿ってたら特に引数は必要ないっぽい。
    print $this->Form->create();

    //フォーム要素を作成。下記の場合はテキストボックスが作成される。
    print $this->Form->input('zipcode' , array('label'=>'郵便番号'));

    //フォームを終了する。
    //下のように文字列を引数で渡したら送信ボタンが作成される。
    print $this->Form->end('送信');
?>

アクセスしてみる。
http://localhost/cakePHPfolder/postal_codes/

簡単なものだね。

Formヘルパーの簡単なまとめはここに多少まとめました
index()アクションをいじる

上の作成の仕方の場合、$this->Form->create()のデフォルトによりPOSTデータがそのままindex()へと送られる。
というわけで、index()内を編集してポストデータが送られたら送られたデータを解析し、別の動作をするよう編集する。

//PostalCodesControler.php内
   public function index(){
        //POSTリクエストの場合の動作
        if($this->request->is('post')){
           $data = $this->request->data['PostalCode']['zipcode'];
           if(!preg_match("/([0-9]{3})\-?([0-9]{4})/i" , $data){
                $this->Session->setFlash("入力が正しくありません。郵便番号は7桁の半角数字か***-****形式で入力して下さい");
           }else{
                $this->redirect(array('action'=>'address',$data));
           }
        }
   }

本当は前方一致で検索できるようにしたいんだけど、まずは7桁での検索から作成してみます。
なお、上記の説明は以下の通り。

$this->request->is('post')

今POSTリクエストかどうかをこれで判断する。なお、$this->request->is()では以下の判別ができるとても便利な関数。

$this->request->is('文字');の主な一覧
postPOSTリクエストか
getGETリクエストか
putPUTリクエストか
deleteDeleteリクエストか
ajaxAjaxリクエストか
mobile携帯からのリクエストか

なお、putに関しては、ビュー出力時に$this->request->dataの中にデータが設定されてあった状態で、フォームがPOST送信された場合になる。
deleteは$this->Form->create()の中で'type'=>'delete'と設定した場合になるっぽい。
(putも'type'=>'put'とすることでそうなる)。

$this->request->data

POSTでの送信内容はこの中に格納される。
なお、上記では配列として取り出しているが、関数で取り出すこともできる。

 $data = $this->request->data('PostalCode.zipcode');

最初のだと、たとえば$this->request->data['PostalCode']['zipcode']が存在しなかった場合(空ではなくそもそもその配列のキーが存在しなかった場合)エラーが起きるから上のように関数で呼び出して存在するかどうか確かめる方がいいかも。

また、この中にデータを格納することで、フォームのデフォルト値を設定することもできる。
(指定の仕方はFormヘルパーの使い方の部分に記載しました)

$this->Session->setFlash()

1回かぎりのメッセージを入力できるみたい。上記の場合なら、もし正規表現のルールに沿ってない場合、

入力が正しくありません。郵便番号は7桁の半角数字か***-****形式で入力して下さい

みたいに出力される。

$this->redirect()

その名前の通り、リダイレクトしてくれる。上記の場合なら、http://localhost/cakephpFolder/postal_codes/address/$dataにリダイレクトされる。
(このURLの場合、cakePHP内で$dataがaddress()アクションの第一引数として渡される)

address()アクションとaddres.ctpファイルを作る

ここでは渡された引数を元に、郵便番号を検索してデータを吐き出す。また、データがなかったらindex()へと戻すように指定する。

(今は7桁でしか郵便番号検索できないようにしてるけど、次回複数検索もできるようにするつもりだから、それを想定して複数の検索結果を表示できるようにしてます)

//Controller/PostalCodesController.php内
public function address($zipcode = 0){
  $options = array('conditions'=>array('zipcode'=>$zipcode));
  if(!$result = $this->PostalCode->find('all',$options)){
    $this->Session->setFlash("ヒットしませんでした");
    $this->redirect($this->referer(array('action'=>'index')));
  }else{
    $this->set(compact('result'));
  }
}



//View/PostalCodes/address.ctp内
<?php 
    $count = 0; 
    $cause = array(0=>'変更なし',1=>'市政・区政・町政・分区・政令指定都市施行',2=>'住居表示の実施',3=>'区画整理',4=>'郵便区調整等',5=>'訂正',6=>'廃止');
    $this->Html->addCrumb('検索',array('action'=>'index'));
    $this->Html->addCrumb('検索結果');
?>
<?php print $this->Html->getCrumbs(">"); ?>
<hr>
<H1>検索結果</H1>
<?php foreach($result as $rows): ?>
【<?php print ++$count; ?>】
<table border='1' style='margin-bottom:20px; width:700px;'>
    <tr><th width='200'>郵便番号</th><td><b style="color:#292999;"><?php print $rows['PostalCode']['zipcode']; ?></b></td></tr>
    <tr><th width='200'>JISコード</th><td><?php print $rows['PostalCode']['jiscode']; ?></td></tr>
    <tr><th width='200'>都道府県</th><td><?php print $rows['PostalCode']['state']; ?>(<?php print $rows['PostalCode']['state_kana']; ?>)</td></tr>
    <tr><th width='200'>市区町村</th><td><?php print $rows['PostalCode']['city']; ?>(<?php print $rows['PostalCode']['city_kana']; ?>)</td></tr>
    <tr><th width='200'>町域名</th><td><?php print $rows['PostalCode']['street']; ?>(<?php print $rows['PostalCode']['street_kana']; ?>)</td></tr>
    <tr><th width='200'>変更</th><td><?php print $cause[$rows['PostalCode']['cause']]; ?></td></tr>
</table>
<?php endforeach; ?>
<?php print $this->Html->link('戻る',array('action'=>'index')); ?>

また、初めて使うのをひとつひとつ紹介していく。

$this->PostalCode->find('all',$options)

これが、モデルのデータ(つまりはMysqlのデータ)を取り出すcakePHP関数となる。実際は

$this->モデル名->find($type , $options)

という書き方になる。
これは、相当色んなことができるっぽいので詳細はFormヘルパー同様また別の機会にまとめるとして、今は$options = array('conditions'=>array('zipcode'=>$zipcode));にて郵便番号を指定し、'all'で検索結果を全部出力するということだけ。

$this->referer(array('action'=>'index'))

これもかなり使える関数。HTTP_REFERERが読み取れた場合はそれを返し、読み取れなかった場合は中の配列を返す。
これを使うことで、あちこちのアクションからこのaddressにアクセスしたとしても、それぞれのアクションへと簡単に返すことができる。

$this->set(compact('result'));

一番使う関数になるのかも。Viewページで使う変数をこの関数で渡す。
この場合は$resultをViewページへと渡すことができる。
(なお、compactはPHP標準関数です。念のため)

$this->Html->addCrumb('検索',array('action'=>'index'));
$this->Html->getCrumbs(">");

これも結構便利そうな関数。トピックパス(俗に言うパンくずリスト)を作成してくれる。

$this->Html->link('戻る',array('action'=>'index'));

直感的に解るけど、リンクを作成できる。これも、また別の機会に詳しい使い方をまとめようかな。

渡される郵便番号データを検索前と検索後に加工

さて、これで郵便番号で住所が検索できる、わけじゃない。
というのも、面倒にも今回郵便番号データをdecimal型にしちゃってるからだ。だから、次にはモデル内にbeforeFind()afterFind()関数を作り、その中で郵便番号データを加工する。
この2つの関数は、作成しておくと検索開始時と検索後の出力時にいろいろな指示を与えられる。

//Model/PostalCode.php内
   public function beforeFind($queryData){
     if(!empty($queryData['conditions']['zipcode']) and preg_match("/^(\d{3})\-?(\d{4})$/i",$queryData['conditions']['zipcode'],$match)){
       $queryData['conditions']['zipcode'] = (int)($match[1].$match[2]) / 10000000;
     }
     return $queryData;
   }

   public function afterFind($result){
     foreach($result as $key => $row){
        if(!empty($row[$this->alias]['zipcode'])
            && preg_match("/^(\d{3})(\d{4})$/i",str_pad($row[$this->alias]['zipcode'] * 10000000 , 7 , '0' , STR_PAD_LEFT), $match)){
         $result[$key][$this->alias]['zipcode'] = $match[1] . '-' . $match[2];
        }
     }
     return $result;
   }
[追記]$this->aliasについて

(2012/05/17追記)
モデル内またはAppModel内で自身のモデルを記載する際、直接記載するのではなくこの変数を使うようにした方がいいみたい。
モデルの名前を別のに指定していた場合でも、これだとエラーが起きなくなる。


これで、$this->PostalCode->find()時にconditions配列の中にzipcodeが指定されていたら小数点の形へと変形してくれ、データを取り出す時には郵便番号の形へと変換してくれる。

検索してみる

さて、これで内部的に変更してくれ、こっちは郵便番号を郵便番号のフォーマットのまま検索できるようになった。
というわけで、東京都中央区(103-0001)を検索してみる。

うまくいった\(^o^)/


公開するかぎりはあまりいい加減なことは書けないし、今回作成する際に相当いろいろと調べて(それがブログを作った目的でもあるんだけど)かなり時間がかかってしまったが、なんとかcakePHP2.1で簡単な検索だけはできるようになった。
次回は、郵便番号を前方一致で検索できるように作り替えます。

本当は住所から郵便番号を検索することが目的だけど、それはまだもうちょっと先の話になりそう。

【参考リンク】
CookBook2.x(英語版)
FormHelper Class Info:
CakeRequest Class Info:

2012年5月10日木曜日

Scaffoldingを使ってみる

前回でテーブルは作成したので、今回から(やっと)cakePHPで郵便番号検索の開発をしはじめてみる。ここからはモデルを作って、コントローラーを作って、ビューを作って…ってしていくのだろうけど、その前にScaffoldingがどういうのかやってみたかったから、今回ちょろちょろっと作成してみた。

Scaffoldingとは

CookBook2.x(英語版)にそこまで詳しくない程度に載ってます。…というより後で気づいたんだけど、書いてる内容はCookBook1.3(日本語版)と殆どかわらなかった。かいつまんで書くと、Scaffoldingアプリケーションを使用するとデータの編集や作成、削除などが簡単にできて作成当初役立つらしい。たしかに、ウェブページを作成する時って必ずといっていいほど最初にいくつかデータを入れてテストするもんね。むしろそうしないと作り始められないし。

ちなみにScaffoldは「足場」という意味で、開発者が最初に使うツールという位置づけなのだそうです。CookBook2.x英語版ではpretty coolやso coolとクールを連発してます。そして1.3の日本語版でも連発してます。

使い方

簡単過ぎてびっくり。

1.コントローラーを以下のように作成する

前回の作成でテーブル名をpostal_codesとしたので、コントローラ名は命名規則に沿うと「PostalCodesController」となる。
そしてクラスの中には$scaffoldのみを記載する。

[app/Controller/PostalCodesController.php]
<?php
   class PostalCodesController extends AppController {
       var $scaffold;
   }
?>
2.おしまい



おしまい?

あら、やだ、簡単。Scaffoldingではモデルを作る必要すらないらしい。
本当にこんなので使えるのかなって半信半疑でアクセスしてみて、またびっくり。
http://localhost/[cakephpFolder]/postal_codes/にアクセスすると・・・

タイトルの部分、「Postal Codes」ってわざわざアンダーバーの部分がスペースに変わってるんだね。cakePHPってそんな小細工もできるのかぁ。
ちなみに、フィールド名の部分をクリックすると昇順/降順でソートもできるし、リストの下の方はこんな感じにちゃんとページナビゲーションと発行したクエリのリストまでついてる。
(発行クエリのリストは、開発中ならどの作成ページにもつくらしいけど)

この「View」ってボタンを押すとこんな感じ。

新規作成と編集画面もなかなか見やすくなってる。

不思議だねぇ。まあ、今回はもうデータは全部入れちゃってるし、これを使う機会はないんだけど。でも、かなり関心した。cakePHPすごいねぇ。

Scaffoldingを使うにあたって

この機能は、あくまでも開発の初期に使用する前提で用意されており、できることは「全体の閲覧」「挿入」「編集」と基本的なことしかできない(当たり前か)。また、$scaffoldを使う場合、他のアクションを入れるとうまく機能しなくなったり、変な動作をしたりするみたい。

まあ、本当に最初の頃にデータを入れる時に使うのであれば全然問題ないよね。あとはせっかくだからカラムやインデックスの作成/編集/削除ができれば最高なんだけど…phpMyAdminみたいに。

(補足)joinテーブルでScaffoldingしてみる

なお、今回は一つのテーブルだけを使ってScaffoldingしたけど、joinテーブルもできるらしい(CookBook1.3日本語版ではなぜかそこの注意点の部分が抜けてる。2.0英語版も1.3英語版も書いてるのに)。郵便番号検索のとは関係ないけど、一応その仕方も調べてみたからまとめておく。

$hasManyと$belongsTo

cakePHPでは、この$hasMany$belongsToといった変数に情報を入力することでJoinを実装するらしい。
他にも方法はいろいろとあるけど、それは別の機会にじっくり調べるとして、今回はやり方だけざっくり書いておく。

2つのテーブルを作成

本当は今回作ったpostal_codesテーブルで作成したかったんだけど、既に12万件データが入ってるなかでjoinしようとしたら重すぎてダメだったんで(どうも、join先のテーブルで全件読み込もうとしちゃうらしい)、新たに簡単なテーブルを作成した。

#testsテーブルを作成
mysql> create table if not exists tests(
    -> id int(11) not null auto_increment ,
    -> title varchar(50) collate utf8_bin not null ,
    -> username varchar(30) collate utf8_bin not null ,
    -> primary key (id)
    -> )Engine=Myisam default character set utf8 collate utf8_bin;
Query OK, 0 rows affected (0.11 sec)

#test_commentsテーブルを作成
mysql> create table if not exists test_comments(
    ->  id int(11) not null auto_increment ,
    ->  created datetime not null ,
    ->  comment varchar(100) collate utf8_bin not null ,
    ->  test_id int(11) not null ,
    ->  primary key (id),
    ->  key test_id (test_id)
    -> )Engine=Myisam default character set utf8 collate utf8_bin;
Query OK, 0 rows affected (0.05 sec)

なお、test_commentsテーブルの「test_id」が、cakePHPの命名規約に沿った外部キーの書き方となる(joinするテーブル名の単数形_id)。
次にコントローラーとモデルを作成。joinをする場合、モデル作成は必須っぽい。

// app/Controller/TestsController.php
<?php
   class TestsController extends AppController {
         public $scaffold;
   }
?>

// app/Controller/TestCommentsController.php
<?php
   class TestCommentsController extends AppController {
         public $scaffold;
   }
?>

// app/Model/Test.php
<?php
   class Test extends AppModel{
         public $hasMany = array('TestComment');
   }
?>

// app/Model/TestComment.php
<?php
   class TestComment extends AppModel{
         public $belongsTo = array('Test');
   }
?>

これで準備完了。命名規約に沿ってつくれば、これだけで全部読み込んでくれる。
$belongsToは外部キーがあるテーブルに、$hasManyは参照されるテーブルに書けばいいらしい。
そして両方のモデルに、それぞれ上のように設定することでScaffoldingでjoinされた結果がでてくる。
(これはあくまでScaffoldingを使用する時の設定です)

あとは、testsまたはtests_commentにアクセスすればいいだけ。いくつか作成したのが下の画像になります。

変な名前は気にしないでください。
testsを開いたんだけど、「New Test Comment」「List Test Comment」ってボタンができてる。
Viewを開くとこんな感じ。

一方、test_contentsを開くとこんな具合に結合がされている。

なお、Titleの部分が表示されてるのは、命名規約でtitleまたはnameというフィールド名がリストタイトルとして認識される為。
Scaffoldingではそのリストタイトルだけが表示されるみたい。
この表示を変えたい場合、モデルのTest.phpに以下のように付け加える。

// app/Model/Test.php
<?php
   class Test extends AppModel{
         public $hasMany = array('TestComment');
         public $displayField = 'username';
   }
?>

すると、usernameのカラムが表示される。

テストだから変な名前なのはスルーしてください。


ついでだから、titleカラムを別の変な名前にしてリストタイトルとして認識できるカラムが存在しない状態にするとどうなるかも確かめてみた。

mysql> alter table tests change title testname varchar(50) collate utf8_bin not null;
Query OK, 0 rows affected (0.06 sec)
Records: 0  Duplicates: 0  Warnings: 0

$displayFieldを消して再度アクセスしてみる。

なるほどー・・・主キーが表示されるのかあ。


まあ、でも最初に使うには便利そう。

本当は郵便番号検索のラベルでエントリしようと思ったんだけど、全然関係なくなってしまったので今回は単一でのエントリとしました。次回から本当に作成しだそ。

【参考リンク】
CookBook2.x(英語版)
CookBook1.3(日本語版)

2012年5月8日火曜日

郵便番号検索を作る─1.テーブルを作成

今まででCakePHP2.1の基礎の部分を勉強してきたんで、そろそろ実践しながら学んでいこうと思う。やっぱり実際に作りながら覚えるのが一番だよね。

こういう場合一番いいのが簡単なブログを作ってみるっていうのだと思うけど、それはCookBook2.xに日本語版でも詳しく載ってるので、何つくろうかなって5秒くらい考えた結果、ブログの次に簡単そうで色んな人もやってる郵便番号検索をぼくも作成してみることにした。

多分、郵便番号検索を作ってみるメリットはこんな感じであると思う。

・大量のデータを扱える
・DBに登録するデータは簡単にDLできる
・ただ郵便番号を検索するだけなら比較的作りが簡単(多分ね)
・後で複雑な検索も付け加えられる(と思う)

今回はあくまでもcakePHPの練習用として作成するわけだし、「データをcakePHPのモデル内で加工する」ってこともやってみたいから郵便番号をdecimal型で保存しようと思う。decimal型にするのは、単に文字列型より検索時に軽くなるっぽいしね。

事前準備──郵便番号データを用意

一応日本郵便のホームページからもダウンロードできるんだけど、いろいろと問題があるっぽいので(郵便番号データの落とし穴)、下記の再配信サービスから「x-ken-all.csv」ダウンロードした。

zipcroud─郵便番号再配信サービス
(郵便番号データ(加工済バージョン)をダウンロード)

なお、日本郵便もだけどzipcloudの郵便番号データも文字コードがShift-JISになってる。
ぼくの開発環境はUTF8だし、そもそもCakePHPのデフォルト文字コードがUTF8だから、ここは変換すべきかも。

$ nkf -w x-ken-all.csv > x-ken-all-utf.csv //UTF形式に変換
$ nkf -guess x-ken-all-utf.csv //ちゃんと変換されたかチェック
UTF-8 (CR)
$ wc ken_all_utf.csv //csvの行数を調べておく
  124650   186766 18042207 x-ken-all-utf.csv

(Windosでの文字コード変換ソフトはVectorで簡単に見つかります)

更にデータを加工する

ダウンロードしたデータは、以下の項目がcsv形式で格納されている。

全国地方公共団体コード
(旧)郵便番号(5桁)
郵便番号(7桁)
都道府県名(カタカナ)
市区町村名(カタカナ)
町域名(カタカナ)
都道府県名(漢字)
市区町村名(漢字)
町域名(漢字)
他よくわからない数字が6つ
(詳細は日本郵便:郵便番号データファイルの形式等に記載してます)

この中で、旧郵便番号はいらない気がするし、よくわからない数字も「更新の有無」と「その理由」以外はあっても使わない気がするから、中身をちょっとsedコマンドとawkコマンドを使って編集する。

//まずsedコマンドでダブルクォーテーションを消す
$ sed -e "s/\"//g" x-ken-all-utf.csv > x-ken-all-utf-s.csv 
//結果を1行だけ表示
$ head -n 1 x-ken-all-utf-s.csv
01101,060  ,0600000,ホッカイドウ,サッポロシチュウオウク,,北海道,札幌市中央区,,0,0,0,0,0,0

//次に、awkコマンドで必要なものだけを取り出す
$ awk -F, '{print ",\""$1"\",0."$3",\""$4"\",\""$5"\",\""$6"\",\""$7"\",\""$8"\",\""$9"\","$14","$15}' x-ken-all-utf-s.csv > x-ken-all-utf-si.csv
//できた結果を1行だけ表示
$ head -n 1 x-ken-all-utf-si.csv
,"01101",0.0600000,"ホッカイドウ","サッポロシチュウオウク","","北海道","札幌市中央区","",0,0

先頭にカンマ(,)を入れたのは、このあとmysqlでLoad Data Infileをするつもりで、auto_incement型の数値を入れる為。「全国地方公共団体コード」はなんとなく文字列型で保存することにします(先頭に0があってヤだし)。

テーブルを作成する

以下の形でテーブルを作成する。

■テーブル名
  postal_codes(命名規則に沿って複数型に)
■フィールド
  id──主キー(int型)
  jiscode──全国地方公共団体コード(varchar型)
  zipcode──郵便番号(decimal型)
  state_kana──都道府県カタカナ(varchar型)
  city_kana──市区町村カタカナ(varchar型)
  street_kana──町域カタカナ(varchar型)
  state──都道府県(varchar型)
  city──市区町村(varchar型)
  street──町域(varchar型)
  changed──最近変更されたか(tinyint(3)型)
  cause──変更理由(tinyint(3)型)
  (以下追加:2012/06/11)住所検索を実装する際に追加しました。
  address──全部の住所(varchar(100)型)


とりあえず、都道府県とかはどれくらいの文字数があるかわからないので、全部varchar(100)で作成しておく。
あとインデックスは最初はzipcodeのみに貼っておく。

mysql> set names utf8;
Query OK, 0 rows affected (0.00 sec)
mysql> create table if not exists postal_codes(
    -> id int(11) unsigned not null auto_increment ,
    -> jiscode varchar(6) collate utf8_bin not null default '',
    -> zipcode decimal(8,7) unsigned not null default 0,
    -> state_kana varchar(100) collate utf8_bin not null default '',
    -> city_kana varchar(100) collate utf8_bin not null default '',
    -> street_kana varchar(100) collate utf8_bin not null default '',
    -> state varchar(100) collate utf8_bin not null default '',
    -> city varchar(100) collate utf8_bin not null default '',
    -> street varchar(100) collate utf8_bin not null default '',
    -> changed tinyint(3) unsigned not null default 0,
    -> cause tinyint(3) unsigned not null default 0,
    -> primary key (id),
    -> key zipcode (zipcode)
    -> )Engine=Innodb default character set utf8 collate utf8_bin comment '郵便番号';
Query OK, 0 rows affected (0.29 sec)
データを挿入

あとは、LOAD DATA IN FILEで一気に挿入するだけ。
注意すべき点は、LOAD DATA IN FILEをする際にはset character_set_database='utf8';みたいに指定しておかないと文字化けしちゃうってこと。

mysql> set character_set_database='utf8';
Query OK, 0 rows affected (0.03 sec)
mysql> load data infile "/x-ken-all-utf-si.csv" into table postal_codes 
    -> fields terminated by ',' optionally enclosed by '"';
Query OK, 124650 rows affected, 65535 warnings (6.86 sec)
Records: 124650  Deleted: 0  Skipped: 0  Warnings: 0

最初に確認した124650行全部はいりました。ということで最後に確認。

mysql> select * from postal_codes limit 10\G
*************************** 1. row ***************************
         id: 1
    jiscode: 01101
    zipcode: 0.0600000
 state_kana: ホッカイドウ
  city_kana: サッポロシチュウオウク
street_kana: 
      state: 北海道
       city: 札幌市中央区
     street: 
    changed: 0
      cause: 0
*************************** 2. row ***************************
(中略)
10 rows in set (0.00 sec)

mysql> select count(*) as cnt from postal_codes;
+--------+
| cnt    |
+--------+
| 124650 |
+--------+
1 row in set (0.10 sec)

バッチリだね。ここまでは、cakePHPと関係あるのは命名規約でテーブル名を複数型にしたことくらいしかないけど、次回はこれを使ってモデルとコントローラーを作成していこうと思います。

2012年5月5日土曜日

cakePHP2.xの命名規約

cakePHPだけでなく、フレームワークには命名規約っていうのがあるらしい。全員がそれに沿って作ることで、作業の分割化がスムーズにできるようになるのだとか。
今までフレームワークを触ったことがないぼくは、つい「面倒くさいな」って思ってしまうんだけど、そうしないわけにもいかないっぽいしね(完全無視でも非効率ながらできるっぽいけど)。そんなわけで、それぞれの命名の仕方をまとめてみた。

(以下、全部CookBook日本語版に書かれてることを単にまとめただけです。hasAndBelongsToManyやcounterCacheといった場合の命名規約はその際に書くとして今回は端折って基本と思える部分だけを書きます)

基本──キャメル型とアンダースコア型、複数型と単数型

cakePHPはこの4つの形を使って命名していくらしい。

■キャメル型
senddataSendData
bigareaBigArea
みたいに、単語の頭をそれぞれ大文字にして書く。殆どがこのキャメル型で記述する。

■アンダースコア型
senddatasend_data
bigareabig_area
単語を連結する際にアンダースコア(_)を入れる。CakePHP1.3まではファイル名はこれで記述するルールだったみたいだけど、2.xからはコントローラーやモデル名はキャメル型に統一され、今はテーブル名とビューファイルだけ(?)をアンダースコア型で記述する。

■複数型での記述
複数型は、厳密に英語の名詞が複数形になるのと同じ変化の形で記述する。 データベースを扱う場合のコントローラー、及びテーブル名は主に複数型となる。
apple → apples
moneymonies
personpeople
childchildren
よくできてるなぁ。なお、2つの単語を組み合わせる場合は
senddatasend_datas

テーブルとフィールドの規約
テーブル名

【アンダースコア型】【複数形】で作成

(例)
applespeoplechildrenbig_cities

newsinfomationというような不加算名詞はそのまま作成する。
※ただし、モデルで使用テーブルを指定する場合は上記の規則に捉われないっぽい。

フィールド名

【アンダースコア型】【単数形】で記載
(別に基本複数でも問題ないっぽいけど単数形じゃないとダメなのもあるから単数形統一がいいかも)

(例)
bodyfirst_namechild_first_name

■フィールド名の規約
主キーid
※モデル内で他の指定も可
外部キー参照元のjoinテーブル名(単数形)_id
peapleテーブルとjoin→person_id
big_citiesテーブルとjoin→big_city_id
作成日時created(datetime型)
レコード作成時に自動で日時が入る
更新日時modified(datetime型)
レコード作成/更新時に自動で日時が更新
見出しtitleまたはname
※モデル内で他の指定も可

なお、cakePHP1.3ではupdatedmodifiedと同じ動作をすると書いてるんだけど、2.xCookBookではその記載は消えてた(拙い英語力で単に見つけられなかっただけかも)。2.0移行ガイドを読んでも削除されたとは書かれてないから使えるかもしれないけど、使わなくても全然問題ないので使わない方向で行きます。

■その他の規約

(名前は何でも)tinyint(1)smallint(1)でブーリアン値と認識
※Mysqlの場合。0か1しか入らなくなるし、検索でも2以上を検索しようとすると自動で1にされてしまう。このおせっかいはtinyint(2)以上にすることで回避できるっぽい。

cakePHPは複合主キーは基本サポートしない
できなくはないけど、いろいろな便利機能は使えなくなるみたい。

主キーは数値型のauto_incrementを指定するか、char(36)を指定
char(36)を指定すると、自動的にユニークなUUIDを作ってくれるようになる。
ただ、MysqlのInnodbを使う場合はインデックス全てに36バイト分の主キーが加わってデータがやたらと大きくなるし、最悪断片化も起きてパフォーマンスがかなり落ちるからInnodbを使う場合はint型で作った方がいいと思う(MyISAMなら問題ない)。

モデルの規約

app/Model/内に作成する

テーブル名を【キャメル型】【単数形】で記載

テーブル名[people]用のモデルを作成する場合
ファイル名Person.php
クラス名Person
【基本の型】
//[Person.php]
<?php
   class Person extends AppModel{

   }
?>

他のリファレンスを見ると、よく中にpublic $name = 'Person';と書いてるのがあるけど、これはPHP4との互換用にあってPHP5以上を使う場合は特に必要ないらしい。
テーブルを指定する場合は$useTable = '[テーブル名]'と記載する。

コントローラーの規約

app/Controller/内に作成する

関連させるモデルクラスの【キャメル型】【複数形】+Controller

モデル[Person]を使用するコントローラーを作成する場合
ファイル名PeopleController.php
クラス名PeopleController
【基本の型】
//[PeopleController.php]
<?php
   class PeopleController extends AppController{

   }
?>

たったこれだけで、Personモデルと関連付けが行える。
なお、モデルを使用しない場合は好きな名前+Controller.phpで作成可能。
また複数のモデルを使用する場合、$usesの配列で指定する(その場合、自身のモデルも記載)。使用モデルを別で指定する場合も$uses内に記載する。

//[PersonモデルとChildモデルを使用する場合]
<?php
   class PeopleController extends AppController{
       public $uses = array('Person' , 'Child');
   }
?>
//[Userモデルを使用する場合]
<?php
   class PeopleController extends AppController{
       public $uses = array('User');
   }
?>
アクションの規約
■アクションとは

コントローラ内に入れる関数。基本は、ひとつのアクションがひとつのページとなる…みたいな認識でいいのかな。いいのだと思う。

cakePHPで作成したアプリケーションは、原則としてURLが

http://localhost/cakeフォルダ/[コントローラ名]/[アクション名]/([引数1]/[引数2]/...)

という形になる(/Confg/routes.php内で変更可能)。
アクションを端折った場合はindex()が呼び出される。index()がないと、エラーになる。
またURLに指定するコントローラ名はキャメル型でもアンダースコア型でもどっちでもアクセスされる。

//簡単なコントローラーの例(Cakeフォルダをcakephpと指定した場合)
<?php
   class PeopleController extends AppController{
        public function index(){
           //(indexでの動作)
           //http://localhost/cakephp/people/index/で呼び出される
           //http://localhost/cakephp/people/でも呼び出し可
        }
        
        public function edit($id = null){
           //(indexでの動作)
           //http://localhost/cakephp/people/edit/1 で呼び出される。
           //上記の場合$idには1が入る。
        }
   }
?>
■アクションの規約

・URLでアクション名を指定しなかった場合index()が呼び出される
これは上で書いた通り。

・内部的に使いたい関数は先頭にアンダースコア(_)をつける。
こうすると、URLで呼び出せなくなる。public function _setresult(){}というような形で記述。

ビューの規約

app/View/内に作成する

フォルダ→コントローラーの【キャメル型】で記載
その中のファイル→アクションを【アンダースコア型】+.ctp

[PeopleController]で使用するビューファイル
ディレクトリ名People
アクションindex() → index.ctp
edit() → edit.ctp
その他の規約
コンポーネント

app/Controller/Component内に作成する

ファイル名【名前(キャメル型)】+Component.php
SampleComponent.php
宣言class SampleComponent extends Component
ビヘイビア

app/Model/Behavior内に作成する

ファイル名【名前(キャメル型)】+Behavior.php
SampleBehavior.php
宣言class SampleBehavior extends ModelBehavior
ヘルパー

app/View/Helper内に作成する

ファイル名【名前(キャメル型)】+Helper.php
SampleHelper.php
宣言class SampleHelper extends AppHelper

最初は覚えるのが億劫だったけど、全体的に見ても規則性があるから結構覚えやすいかもっていうのが印象でした。

なお余談だけど、この複数型/単数型の読み取りと変換はlib/Cake/Utility/Inflector.phpで行ってるようで、ざっと見てみたところ、イレギュラー変換用のリストの中に、person => peopleの上にはpen○sが、その下にはs○xが並んであった。
どうでもいいんですけどね。イレギュラーのリストはちゃんとアルファベット順に並んであるから単純にそうなるものってだけかもしれないし、ただ上のルールリストの中にもちゃんとperson→peapleとなるような正規表現が記述されてあって、ちょっと気になったってだけですから。それにこのsexは性別を表してるっぽいし。


[参考リンク]
CakePHP2.x CookBook(英語版)
CakePHP2.x CookBook(日本語版)
CakePHP1.3 CookBook(日本語版)