【Contact Form 7】複数フォーム＆選択項目が頻繁に更新されるフォームでのバリデーション

## 概要

ContactForm7（以下、CF7）で作った問い合わせフォームでは、 `wpcf7_validate` をフックしてバリデーションを実施できます。詳細は以下参照。

https://qiita.com/kd9951/items/1717fc384c16e00d6458

### ここでの問題点

以下のような場合、厄介なことになりそう（なった）
* CF7で作成したフォームが同一サイト内で複数存在する
* フォームを複製作成してしまい、name値が同じでも、inputの種類や選択項目が違う
* チェックボックスなどの選択項目の内容が、頻繁に更新される

特に、選択項目の評価基準となるものがバリデーション関数内にベタ書きだと、ここの処理も更新作業が発生します。
そのため、常に最新のフォームから選択項目を参照する必要があります。

```functions.php
add_filter('wpcf7_validate', 'my_form_validate', 11, 2);
function my_form_validate( $result, $tags ) {
	$form = WPCF7_Submission::get_instance();
	if($form) {
		foreach( $tags as $tag ) {
            $type = $tag['type'];
            $name = $tag['name'];
            $post_data = $_POST[$name];
        switch($type) {
            case 'text':
        	case 'text*':
		        //何か処理入れる
        	    break;
            case 'select':
	        case 'select*':
		        $list = ['AAA', 'BBB', 'CCC'];
		        // ↑選択項目と同じ項目リスト。問い合わせフォームが更新されたら修正
		        if( !in_array( $post_data, $list ) ){
			        $result->invalidate($name, "エラー" );
		        }
	        break;
	      }
    	}
  }
  return $result;
}
```

### Formデータを取得

ということで、フォームを表示するためのフォームデータを取得し、そこから選択項目などを引っ張ってきます。

https://qiita.com/gips_nita/items/5936d752f9cb2bd6f0f9

CF7のカスタムバリデーションの記事を参考に判定処理を書くとこんな感じに（関数名が長い＆ダサいので、適宜変更を推奨）。

##  まとめたコード

```functions.php

// CF7の投稿IDからフォームデータを返す
function my_get_wpcf7_form_values_by_id( $id ) {
  $form = wpcf7_contact_form($id);
  $res = [];
  if($form) {
    $manager = WPCF7_FormTagsManager::get_instance();
    $form = $form->prop( 'form' );
    $form = $manager->replace_with_placeholders( $form );
    $form = $manager->restore_from_placeholders( $form );
    $form = $manager->replace_all( $form );
    $res = $manager->get_scanned_tags();
  }
  return $res;
}
// inputのname値から選択項目を返す
function my_get_form_values_by_name( $name, $tags ) {
  $res = [];
  if ( !is_array($tags) ) return $res;
  if($tags) {
	  foreach( $tags as $tag ) {
	    if( $tag->name == $name) {
	      $res = $tag->values;
	    }
	  }
  }
  return $res;
}

// バリデーション処理
add_filter('wpcf7_validate', 'my_form_validate', 11, 2);
function my_form_validate( $result, $tags ) {
  $form = WPCF7_Submission::get_instance();
  if($form) {
    $contact_form = WPCF7_ContactForm::get_current();
    $contact_form_id = $contact_form->id;
    $contact_tags = my_get_wpcf7_form_values_by_id($contact_form_id);
    
    $invalid_required = '必須項目に入力してください。';
    $invalid_input = '入力内容に誤りがあります。';

    foreach( $tags as $tag ) {
      $type = $tag['type'];
      $name = $tag['name'];
      $post_data = $_POST[$name];
      $option_values = my_get_form_values_by_name($name, $contact_tags);

      switch($type) {
        case 'text':
        case 'text*':
          // require
          if( $type == 'text*' && $post_data == '') {
            $result->invalidate( $name, $invalid_required);
          }
          // TODO: その他処理追加
          break;
        case 'select':
        case 'select*':
        case 'radio':
        case 'radio*':
          if( in_array( $type, array('select*','radio*') ) && $post_data == '' ) {
            $result->invalidate( $name, $invalid_required );
          }
          if( !in_array( $post_data, $option_values ) ) {
            $result->invalidate( $name, $invalid_input );
          }
          break;
        case 'checkbox':
        case 'checkbox*':
            if( $type == 'checkbox*' && is_null($post_data) ) {
              $result->invalidate( $name, $invalid_required );
            }
            if($post_data) {
              foreach( $post_data as $data ) {
                if( !in_array( $data, $option_values ) ) {
                  $result->invalidate( $name, $invalid_input );
                }
              }
            }
          break;
        case 'email':
        case 'email*':
              if( $type == 'email*' && $post_data == '' ) {
                $result->invalidate( $name, $invalid_required);
              }
              if( !empty($post_data) && !is_email($post_data) ) {
                $result->invalidate( $name, $invalid_input );
              }
          break;
        case 'tel':
        case 'tel*':
            if( $type == 'tel*' && $post_data == '' ) {
              $result->invalidate( $name, $invalid_required);
            }
            if( !empty($post_data) && !wpcf7_is_tel( $post_data ) ) {
              $result->invalidate( $name, $invalid_input );
            }
          break;
        case 'textarea':
        case 'textarea*':
          if( $type == 'textarea*' && $post_data == '' ) {
            $result->invalidate( $name, $invalid_required );
          }
          break;
        case 'number':
        case 'number*':
          if( $type == 'number*' && $post_data == '' ) {
            $result->invalidate( $name, $invalid_required );
          }
          if ( !empty($post_data) && !wpcf7_is_number($post_data) ) {
            $result->invalidate( $name, $invalid_input );
          }
          break;
        case 'date':
        case 'date*':
          if( $type == 'date*' && $post_data == '' ) {
            $result->invalidate( $name, $invalid_required );
          }
          if ( !empty($post_data) && !wpcf7_is_date($post_data) ) {
            $result->invalidate( $name, $invalid_input );
          }
          break;
      }
    }
  }
  return $result;
}
```

## ポイントなど
### $_POSTからが推奨らしい
バリデーション系の記事を読み漁っていくと、`WPCF7_Submission`クラスの`get_posted_data()`で送信内容を入れるパターンもありましたが、これはプラグイン内でサニタイズしたデータだそうです。
CF7も生の$_POSTで検証を奨めていたので、そちらを採用しました。

https://contactform7.com/2020/07/28/accessing-user-input-data/

### 選択項目のバリデーション

`my_get_form_values_by_name($name, $contact_tags);` で選択項目が配列で返ってくるので、ベタ書きしたリスト配列から置き換え。
※CF7は標準でjsでのバリデーションを行っているようですが、所謂「悪意あるユーザー」対策として、選択項目に対しても厳密なバリデーションを行っています。

```php
if( !in_array( $post_data, $option_values ) ) {
    $result->invalidate( $name, $invalid_input );
}
```

### 判定処理の関数
ここは個人の好みの問題だったり、状況に応じて書いていただきたいところですが、今回はWPとCF7内にあったバリデーション関数を使ってみました（長いものには巻かれろ精神）。

* `is_email($post_data)`
* `wpcf7_is_tel( $post_data )`
* `wpcf7_is_date($post_data)`
など