WHAT'S NEW?
Loading...

CakePHP用寝ぼけたバリデーションメッセージを起こす

CakePHPのバリデーションは非常に便利だ。1.1時代はかなりショボすぎる感があったけど、1.2になって、直感的にかなり分かりやすいものになった。

具体的に例を出すと、例えばメールフォームがあたっとして、内訳が
  • 件名:subject
  • 宛先:to
  • 本文:body
があったとする。
それらにバリデーションを入れる場合、例えば件名は空禁止、toはemail形式、本文は最低5文字(実際には5バイト)入力するのを強制する場合は、以下のような内容でモデルを設定することになる。
class Mail extends AppModel {
  public $name = 'Mail';
  public $validate = array(
    // 件名のバリデーション
    'subject' => array(
      // 空入力
      array(
        'rule' => array('notEmpty'),
        'message' => '件名を入力してください',
        'last' => true,
        'required' => true,
      ),
    ),
    // 宛先のバリデーション
    'to' => array(
      // email形式
      array(
        'rule' => array('email'),
        'message' => '正しいメールアドレスを入力してください',
        'last' => true,
        'required' => true,
      ),
    ),
    // 本文のバリデーション
    'body' => array(
      // 入力文字数
      array(
        'rule' => array('minLength', 5),
        'message' => '最低5バイトは入力してください',
        'last' => true,
        'required' => true,
      ),
    ),
  );
記事を書いた後CakePHP武将のecworks_masap氏に言われて気がついたんだけど、1点注意!「5バイト」なんて文字は運用では使わない。ユーザに対して「バイト」なんて単位を書くのはおかしいからだ。しかしここではあくまで例として(minLengthを使ったため)コードとの整合性を重視し、あえて「5バイト」と書いてみた。
各フィールド名の中には複数の条件を入れることができる。そしてそれらの条件1個1個に名前を付けることもできるが、今回は1つのフィールドに1個しか条件を入れてないので、特に名前は付けていない。

だから、'subject' => array()の中にいきなりarray()で条件をかいている。

各条件のコメント分にも書いてあるような内容を名前にしておくとあとで便利になる場合がある。もしくは厳密に名前を独自に付けることによるコントローラ側でのバリデーション判別などする場合も便利になる。具体的には以下のように、いきなり
array()ではなく、名前を指定してarray()を代入する。
'subject' => array( 'empty_case' => array( 'rule' => ・・・・・・ 'required'=> true));

さて、これらのメールフォームを実際にどうやって画面に表示させるかというと、大体こんなビューファイルになるんじゃないかと思う。
<?php echo $form->create('Mail', array('url' => array(
  'controller' => 'mails',
  'action' => 'index',
  'id' => null
)))?>

<dl>
  <dt><label><?php __('件名')?></label></dt>
  <dd><?php echo $form->input('Mail.subject', array(
    'div' => false,
    'label' => false,
  ))?></dd>
  <dt><label><?php __('宛先')?></label></dt>
  <dd><?php echo $form->input('Mail.to', array(
    'div' => false,
    'label' => false,
  ))?></dd>
  <dt><label><?php __('本文')?></label></dt>
  <dd><?php echo $form->input('Mail.body', array(
    'type' => 'textarea',
    'div' => false,
    'label' => false,
  ))?></dd>
</dl>

<?php echo $form->submit(__('次へ &raquo;', true), array(
  'escape' => false,
))?>

<?php echo $form->end()?>
このビューをブラウザで表示するとこのような画面になる。
まぁこれといって特徴の無い普通のフォームだ。
バリデーションのルールでminLengthというものがある。これは最低何バイト入力させるかという設定だ。同じようにmaxLengthというルールもある。しかしこれらはバイト単位なので、仮に5と設定している場合、日本語で「おはよう」と入力すると、4文字なのにバイト数にすると(utf-8だと)12バイトになるので、とたんにクリアしてしまう。

これを避けるためには、全モデル共通のapp_model.phpをapp/直下に設置し、マルチバイト用のminLength、maxLength、そしてbetweenなどのメソッドを用意しておくべきだ。

その際のメソッド名だが、よくいろいろな人のブログ記事に書いてあるパターンだと、
  • minLength_jp()
  • maxLength_jp()
などが見受けられる。

どうしてjpを付けるのかがよくわからない。中国語や韓国語などのマルチバイト文字はバリデーションエラーにさせ、日本語のみを入力させたいのであればこの名前で良いと思うが、そんなことするケースはまず無いと思う。

マルチバイトでやるんだから、単純にphpメソッド風に頭にmb_つけて
  • mb_minLength()
  • mb_maxLength()
でいいんじゃないかとおもうんだけど、みんなそれほどネーミングにこだわりが無いのかね。

ネーミングはすごく大事なので、隣の人に嫌われるくらいにこだわった方が良い。
さて、この状態ではまだコントローラに何も書いてないので、「次へ」ボタンをクリックしても、ほんとに何も起こらない。
というわけで、コントローラを書いてみる。mailsコントローラだ。
public function index()
  {
    // 投稿された場合
    if(!empty($this->data)) {

      // postデータとモデルをバインド
      $this->Mail->set($this->data);

      // モデルのバリデーションを起動
      $this->Mail->validates();

    }
  }
これで、投稿した場合にちゃんとバリデーションが起動するようになる。
モデルのset()メソッドを実行しておかないと、コントローラでバリデーションを動かせないので注意だ。

で、早速何も入力しないで「次へ」ボタンをクリックしてみよう。
正しくバリデーションエラーが表示された。
そのようにコーディングしたので当然といえば当然なのだが、個人的にはいつも嬉しくなる。

さすがCakePHP!
俺達にできないことを
平然とやってのけるッ!
そこにシビれる!
あこがれるゥ!

とまぁ、やっぱり嬉しいわけだが、こういったフォームのバリデーションエラーがたまに憎くなる時がある。

それがどういうときなのかを説明したい。

まずはビューファイルを以下のように変更してみよう。
<dl>
  <dt><label><?php __('件名')?></label></dt>
  <dd><?php echo $form->input('Mail.subject', array(
    'div' => false,
    'label' => false,
  ))?>
  <ul>
    <li><?php __('件名は必ず入力してください')?></li>
  </ul>
  </dd>
  <dt><label><?php __('宛先')?></label></dt>
  <dd><?php echo $form->input('Mail.to', array(
    'div' => false,
    'label' => false,
  ))?>
  <ul>
    <li><?php __('メールアドレスを入力してください')?></li>
  </ul>
  </dd>
  <dt><label><?php __('本文')?></label></dt>
  <dd><?php echo $form->input('Mail.body', array(
    'type' => 'textarea',
    'div' => false,
    'label' => false,
  ))?>
  <ul>
    <li><?php __('短すぎる本文は送信できません')?></li>
  </ul>
  </dd>
</dl>
このビューに変更すると、ブラウザ上では以下のような表示になる。

どうしてddの中にulをいれて注意書きにしているのか、かというと、ちゃんと意味がある。
バリデーションエラーのフィールドに対するルールの個数とあわせてliタグを表示させているんだ。
subjectのルールを1個増やすなら、liもあわせて1個増やして対応、という形にできる。
だから最初からリスト形式でタグを書いておくんだ。
さぁ、この状態でバリデーションエラーのメッセージを表示させてみよう。
なんか変だよな。
わからんか?

分からん場合、ビューファイルを以下のように編集しなおしてみると良い。
<dl>
  <dt><label><?php __('件名')?></label></dt>
  <dd><?php echo $form->input('Mail.subject', array(
    'div' => false,
    'label' => false,
  ))?>
  <span><?php __('件名は必ず入力してください')?></span>
  </dd>
  <dt><label><?php __('宛先')?></label></dt>
  <dd><?php echo $form->input('Mail.to', array(
    'div' => false,
    'label' => false,
  ))?>
  <span><?php __('メールアドレスを入力してください')?></span>
  </dd>
  <dt><label><?php __('本文')?></label></dt>
  <dd><?php echo $form->input('Mail.body', array(
    'type' => 'textarea',
    'div' => false,
    'label' => false,
  ))?>
  <span><?php __('短すぎる本文は送信できません')?></span>
  </dd>
</dl>
この状態でブラウザで見てみるとこのようになる。
これはこれですっきりしていてよろしいかとおもう。
まぁデザインセンスの問題なので、別にこうしたほうが良いというわけではないので、あなたはマネすぐ必要もない。

で、この状態で「次へ」ボタンをクリックしてみるとどうなるのか。これが今回の本題になる。
実際にはこうなってしまう。
先程の画像の流用ではない。れっきとした、直前のフォームを投稿した場合の結果だ。
確実に完全におかしいだろ、この表示は。

どういう事かというと、CakePHPが平然とやってのけているバリデーションメッセージの、表示されるべき位置とHTMLタグがおかしいということになる。

この場合、inputタグの直後にブロック要素であるdivタグでバリデーションエラーが表示される仕組みになっているので、inputのあとに何かタグを書いておくと、それらの間に改行が入り、無理やりエラーメッセージが中国人みたいに割り込んでしまう。

こりゃたまらんぜ。

そしてこれをどうにかしようとして、CakePHPのコアファイルを直接編集したりするのはハックでもなんでもないバッドノウハウだ。

考え方をCakePHPの外に放出させ、柔軟に考えて見れば、もっと簡単にできる方法が見つかる。
結論、こんなものはクライアントサイドにやらせてしまうのが一番よい。

その方法を伝授するので、もし気に入ったらtwitterで俺をフォローしてくれ。

jQueryを使うので、/layouts/default.ctpなどで予め読み込んでおく。
<?php echo $javascript->link('jquery-1.4.2.min')?>
CakePHP1.3では以下になる。
<?php echo $html->script('jquery-1.4.2.min')?>
そしてビューファイルの下の方にでも、以下のコードを記述してみて欲しい。
<script type="text/javascript">
$(document).ready(function(){

  $('div.error-message').each(function(){
    $(this).clone().appendTo($(this).parent());
    $(this).remove();
  });

});
</script>
このコードを書いておくだけで、平然とやってのけられたエラーメッセージを、常に親要素の最後に移動してくれる。

つまり注意書きなどのレイアウトには影響がでないというわけだ。
※だからフォームはtableやdlなどで囲っておかないといけないが、普通そうするだろう
jQueryなので、他にもいろいろ使い方があると思う。ぜひ導入していただきたい。

そして、常に無理やりサーバサイド(php)でなんとかしようとして時間を割くのは今後は避けたほうが己のためになる。

手際よく終わらせて定時に帰ろう。
そしてみんなで飲もうじゃないか。

そのためには無駄な事をしてはいけないんだ。

ではさいなら。