タイトルの通り、要素数が変動する項目のバリデーションチェックを行いたいのですが、(どこかでやったことがありそうで)知らなかったため、備忘録の意味を込めてブログに書いてみようと思います。
条件
<form>
<div>
<div><input type="text" name="itemList[1]"></div>
</div>
<div><input type="submit" value="送信"></div>
</form>
こんな感じのHTMLで以下の条件を満たします。
- itemListは複数個(少なくとも1個)POSTされる。
※ 当たり前ですが、複数個送信される場合は、itemList[2]
みたいな感じで増えます。
※ 送信される要素数はjs側で制御しているため、事前にわかりません。 - エラー時は、各項目ごと(
itemList[1]
、`itemList[2]
`)にエラーメッセージを表示する。(各入力値も復元)
ネストしたバリデーター
CakePHP(>=3.0.5)では、バリデーションをネストすることができます。
長くなるので、詳細はリンク先を見ていただくとして、今回のケースでは、事前に要素数がわからないので利用することができませんでした。
既存機能だけでは難しそうだったため、とりあえず、エラーメッセージを返してくれるValidation::errors()関数の実装から追っていきたいと思います。
Validator
- Validator::errors()
リンク先の処理を追ってみると、202行目あたりの_processRules()関数で各フィールドごとのルール類をまとめて実施しているようです。次に_processRules()関数の実装を見てみようと思います。 - Validator::_processRules()
さらに処理を追っていると2398行目あたりで、ValidationSetオブジェクトをループさせ、各バリデーションを実施しているようです。2404行目では、ネストしたバリデーション時用の処理(子バリデーターで実施した結果のエラーメッセージを親バリデーターの結果として代入)を行っているようです。
これを見ている限り、Validator::add()関数で指定するエイリアスにValidator::NESTEDを指定した後、カスタムバリデーションでエラーメッセージを配列で返せるよう記述すると動くかも!!と思い、こんな感じで書いてみました。
- ValidatonForm.php
protected function _buildValidator(Validator $validator) { $validator ->requirePresence('itemList') ->add('itemList', Validator::NESTED, [ 'rule' => function ($value, $context) { $errors = []; foreach ($value as $id => $text) { $isValid = Validation::notBlank($text); if (!$isValid) { $errors[$id]['_empty'] = '必須入力です。'; } } return (count($errors) === 0) ? true : $errors; }, ]); return $validator; }
- AppController
public function multi() { $form = new ValidationForm(); $data = $this->request->getData(); if ($this->request->is(['patch', 'post', 'put'])) { $form->validate($data); } $this->set(compact('form', 'data')); }
- multi.ctp
<?= $this->Form->create($form); ?> <?php // 初期表示:1行分のみを表示 // それ以外の場合:リクエストデータから入力値を復元(復元できない場合は初期表示と同じ) $itemList = (isset($data['itemList']) && is_array($data['itemList'])) ? $data['itemList'] : ['1' => '']; ?> <?php foreach ($itemList as $id => $value) : ?> <div> <?= $this->Form->control('itemList.' . $id); ?> </div> <?php endforeach; ?> <?= $this->Form->submit('送信'); ?> <?= $this->Form->end(); ?>
こんな感じで書いてみると、確かに1行単位のバリデーションを行うことができるようになりました。
ただ1点問題があります。HTMLが改ざんされ、itemListがPOSTされない場合です。
ちなみに、未入力の場合、HTMLが改ざんされた場合、フォームから取得したエラー情報はそれぞれ以下のようになります。
// debug($form->getErrors());
// 未入力の場合
[
'itemList' => [
(int) 1 => [
'_empty' => '必須入力です。'
],
(int) 2 => [
'_empty' => '必須入力です。'
],
(int) 3 => [
'_empty' => '必須入力です。'
]
]
];
// HTMLが改ざんされた場合
[
'itemList' => [
'_required' => 'This field is required'
]
];
なので、コレジャナイ感がありますが、ctpにこんな感じの記述を追加して対応しました。
<?= $this->Form->error('itemList._required'); ?>
以上。