Cake兄さんがrequired属性に興味を示したようです(僕は妹属性とかの方が……)

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
たまにはそういう機能もあるさ

おまとめ三行

Cake兄さんが勝手にrequired属性をつけてくる
思いきってFormヘルパーを書き換えちまったい
便利だとは思ってるんだよ。ほんとだよ?

HTML5の場合、テキストボックスとかにrequired属性ってのを指定することができますね。

<input type="text" name="name" required="required" />

僕はあまり使ったことないんですけど、この属性をつけとくと、自分でバリデーションチェックのコードを書かなくても必須入力のチェックをしてくれます。データをsubmitするときに、こんなメッセージが出る。

これがrequired属性の力か……

開発者的にはちょっと楽になります。まあ、IE8とかだと反応してくんないですけど。

ところで、CakePHPはFormヘルパーでテキストボックスとかをいろいろ良い感じに出力してくれますが、どうやらCakePHP兄さん……CakePHP2.3は、このrequired属性を勝手につけてくれるようになったみたいなんですね。全く気づきませんでした。bootstrap.jsとかのプラグインのどれかが気を利かせて勝手にやってるんだろうと思ってて、でも僕的には要らない属性だったから何とかキャンセルしようと思っていろいろjsファイルをこねくり回してたんだけど一向になくならず……結構な時間を費やした後に、どうやらCakePHPがやっているらしいってことに気づきました。いや〜まいったまいった。

や、まあ、こっちとしてもそのフィールドは必須入力の項目にしているわけだから、required属性があっても基本的に困ることはないし、自分でいちいち書かなくても良いっていう点では確かに便利なんですけど、たま〜に弊害が出る。「そういうのが出ちゃうってことは、お前の作り方に問題があるんじゃん?」っていう話かもしれないけど、でも出るんだから仕方ない。

今日はその弊害が出たっつー話をば。

あ、ちなみに、もしかしたらバージョンアップに伴ってこれから話す部分の仕様が変わってるかもしれないから、今回はバージョン2.3.10の場合ってことで。2.3.10ってことは「Cake兄さん…」だね(…はここでは「てん」と読む)。遠くの地にいる兄を想う妹が星空を眺めながらつぶやくようなニュアンスで。何言ってるかよく分からないって人は、気にせず先に進みましょう。俺も自分で何言ってるのかよく分からなくなってきた。



required属性がつく場合を検証する

僕はだいたいどんなときでも妹属性とかに興味を示しますが、Cake兄さんがrequired属性を興味を示すのはどんなときかっていうと、簡単にいえば「バリデートルールが設定されていて、かつallowEmptyがない(あるいはfalse)」場合です。Formヘルパーの中にある「_introspectModel()」「_isRequiredField()」「_initInputField()」メソッドあたりを読んだ限り、そんな感じの処理になってるっぽい。

例えば今、以下のようにモデルにバリデートルールを書いて、ビューでテキストボックスを出したとする。

//モデル
public $validate = array(
  'name' => array(
    'rule' => 'notEmpty',
    'message' => '未入力です',
  );

  'email' => array(
    'rule' => 'email',
    'message' => '形式が不正です',
    'allowEmpty' => true,
  );
);

//ビュー
echo $this->Form->text('name');
echo $this->Form->text('email');
echo $this->Form->text('tel');

//出力結果
<input type="text" name="name" required="required" />
<input type="text" name="email" />
<input type="text" name="tel" />

出力されるテキストボックスは、nameはrequired属性がつくけど、emailとtelはつかない。telはバリデートのルールが設定されていないから、emailはルールは設定されているけどallowEmptyをtrueにしてるから。

別に問題はないっすよね。nameは必ず入力してもらわないとデータの登録ができないんだから、PHP側でエラーチェックしようが、データをsubmitする前にjsでエラーチェックしようが、同じことです。



弊害って何よ?

問題は「allowEmptyがfalseの場合はrequired属性がついちゃう」ってとこ。ちなみにでデフォルトはfalse。

早い話が、自分で独自に作ったバリデートルールとかに対しても、allowEmptyをつけてなかったら、required属性がついちゃうんですね。

例えば、基本的には未入力でも良いんだけど、特定の場合にのみ入力を必須にしたいってな場合、自分でルールを作る場合があるじゃないですか。

んー……じゃあ、例えば、性別のラジオボタンを追加して、男性だったら年齢の入力を必須にするみたいなフォームを作ったとしましょう。女性に年齢を聞くのは失礼ってよく言いますからね。

//モデル
public $validate = array(
  'age' => array(
    'rule' => 'originRule',
    'message' => '年齢が未入力です',
  );
);

public function originRule($age) {
  //年齢(データは配列で来る場合があるので)
  $age = is_array($age) ? array_shift($age) : $age;

  //性別
  $sex = $this->data[$this->name]['sex'];

  //男性かつ年齢が未入力ならfalse
  if($sex == 'man' && empty($age)) {
    return false;
  }

  //それ以外はtrue
  return true;
}

//ビュー
echo $this->Form->radio('sex', array('man' => '男性', 'woman' => '女性'));
echo $this->Form->text('age');

すごいざっくり書いちゃったけど、こんなんでエラーチェックができる。

でもこの場合、年齢のテキストボックスにrequired属性がついちゃうんで、女性にチェックを入れてるときでも、年齢が未入力だとはじかれちゃうんですね。だからといってallowEmptyをtrueにすると、別に男性のときでも未入力がエラーにならない。js側でもphp側でも必須のチェックができない状態になっちゃう。



対処法は簡単

まあ、弊害が出たっつっても、それを回避する方法はちゃんと用意されていますから、慌てることはないんですけどね。僕の場合は、知らなかったから焦っちゃったってだけで。

一つは、各個に「required => false」をつける。

echo $this->Form->text('age', array('required' => false));

こうしとけば、出力されるテキストボックスにはrequired属性がつかない。

あとは、createメソッドで「novalidate => true」ってやっとけばrequired属性がつかなくなるらしいんだけど、僕がやったら特に何の効果もなかったのは何でだぜ?

echo $this->Form->create('Model', array('novalidate' => true));

こんなんで良いって書いてあるんだけどなぁ……でもFormヘルパーのソースを見る限り、novalidateなんてオプションが有効になるような処理は書いてないようにも見える。もしかしたら、2.3のマイナーバージョンがさらに上がったやつにはあるのかな?



兄さんんには頼らねえ!って人はこっち

一応、上記のやり方でrequired属性を消すことはできるんだけど、でも僕の場合、これをやるには修正箇所があまりにも多かったんで、「required属性とか知るか! 俺は兄さんには頼らない! エラーは自分でチェックする!」ってな感じで、Formヘルパーのrequired属性をつける処理をコメントアウトしてしまいました。エラーは自分でチェックとか良いつつ、その自分でチェックしてる処理は完全に兄さんに頼ってますけどね。

「_initInputField()」っていうメソッドの中で、該当の処理をコメントアウトすれば、required属性がつかなくなる。

//Formヘルパー
protected function _initInputField($field, $options = array()) {
  if (isset($options['secure'])) {
    $secure = $options['secure'];
    unset($options['secure']);
  } else {
    $secure = (isset($this->request['_Token']) && !empty($this->request['_Token']));
  }

$disabledIndex = array_search('disabled', $options, true);
  if (is_int($disabledIndex)) {
    unset($options[$disabledIndex]);
    $options['disabled'] = true;
  }

  $result = parent::_initInputField($field, $options);
  if ($this->tagIsInvalid() !== false) {
    $result = $this->addClass($result, 'form-error');
  }
  if (!empty($result['disabled']) || $secure === self::SECURE_SKIP) {
    return $result;
  }

  //ここをコメントアウト
  //if (!isset($result['required']) &&
  //  $this->_introspectModel($this->model(), 'validates', $this->field())
  //) {
  //  $result['required'] = true;
  //}

  $this->_secure($secure, $this->_secureFieldName($options));
  return $result;
}

うちのウェブサイトにrequired属性は一切いらねえって人は、こんなことやっちゃっても良いんじゃないでしょうか。



display:noneに注意

これはCakePHPとか関係ない完全な余談なんですが、required属性がついているテキストボックスとかを、CSSで隠している場合。

<div style="display:none">
  <input type="text" name="name" required="required" />
</div>

このとき、submitボタンを押しても、「このフィールドを入力してください」っていう例のメッセージが画面上に現れない。パッと見は何も起こらず、ただただsubmitができないように見える。

僕も、こんな感じの処理をいくつかやってましてね。さっきの性別と年齢の例で言うと、ラジオボタンで男性にチェックがついたらテキストボックスを表示して、女性にチェックがついたらdisplay:noneで隠す、みたいな。このとき、required属性がついてると、女性にチェックをつけてデータ送信ってやった場合、画面が何も変化しない。でも実は「必須入力だぞってメッセージを出したいけど、対象のテキストボックスにフォーカスできねっす」みたいなjavascriptエラーが出てる。






無駄に話が長くなっちゃいましたが、まあ、つまりそういうことです。

たぶん、今後CakePHPやHTMLが進化していくと、今よりもっと、自分では余計なことをやらなくても彼らが気を利かせてやってくれるようになるんでしょうが「でっかいお世話です」って言いたくなることも、その分増えるかもしれないですね。

まだコメントはいただけてないみたい……
もしかしたら何か関連しているかも?