PHP Logo PHP メールフォームの作り方

PHP を使った基本的なメールフォーム(コンタクトフォーム)の作り方についての覚書です。以下で取り扱っている例は確認画面なしのコンタクトフォームです。

入力された値の検証は基本的に PHP で行いますが、クライアントサイドの検証として HTML5 の検証機能や JavaScript を使っています。

また、再読み込みによる二重送信の防止や自動返信の方法、PHPMailer を使ったメールの送信方法、reCAPTCHA v2/v3 を使ったスパム対策の実装方法についても記載しています。

[更新 2021/11/11] クライアントサイドの検証を jQuery から JavaScript に変更し、POST メソッドなどで送信した値は filter_input() を使うように書き換えました。

[更新 2024/03/25] PHP8.1 からは trim() などに null をに渡すと Deprecated エラーになるので、 filter_input() の返す値が null の場合は空文字列に変換するように変更しました。

関連ページ:

更新日:2024年03月25日

作成日:2020年3月27日

基本的なコンタクトフォーム

一般的なコンタクトフォームは form 要素(HTML)を使って記述する入力フォームと PHP などのサーバ側の言語で入力値の検証やメールの送信処理などを記述するスクリプトで構成されています。

以下の例ではサーバ側の処理は PHP を使っています。

入力フォーム(HTML)
  • HTML の form 要素を使って記述
  • form 要素の action 属性に送信先の PHP ファイルを指定(自分自身へ送信することも可能)
  • method 属性で送信メソッド(POST や GET)を指定
  • HTML5 を使うと入力時や送信時に検証を行うことが可能

関連ページ:HTML フォームの設置

サーバ側の処理(PHP)
入力フォームから送信された値を受け取り以下のような処理を実行します
  • 入力された値を検証(正しい形式かや不正な値がないかなど)
  • 問題がなければ mb_send_mail() などを使ってメールを送信

関連ページ:PHP を使ったフォームの操作

JavaScript や jQuery を使えば、値がサーバへ送信される前に検証を行うことができます。

関連ページ:

以下は1つのファイルで作成する入力画面のみのコンタクトフォームの例です。

HTML5 の検証機能を使ったフォーム

HTML5 のフォームの検証機能と PHP を使って値を検証するコンタクトフォームです。

HTML5 のフォームの検証機能を使っているので HTML5 に対応していないブラウザ(caniuse.com)では送信前の検証は機能しませんが、PHP でサーバー側の検証を行います。

必須項目に入力して送信ボタンをクリックするとメールを送信し、問題なく送信できれば「送信成功」と同じページに表示し、何らかの理由で送信できなければ「送信失敗」と表示します。

以下のサンプルでは実際にはメールは送信されません。

サンプルを別ページで開く

以下がコードの全文です。

contact.php
<?php
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';

// 値が null であれば、空文字列に変換する関数(trim() に null を渡すとエラーになるので)
function nullToString($val) {
  if($val === null) return '';
  return $val;
}

//POSTされたデータの前後にあるホワイトスペースを削除してを変数に格納
$name = trim( nullToString(filter_input(INPUT_POST, 'name')) );
$email = trim( nullToString(filter_input(INPUT_POST, 'email')) );
$tel = trim( nullToString(filter_input(INPUT_POST, 'tel')) );
$subject = trim( nullToString(filter_input(INPUT_POST, 'subject')) );
$body = trim( nullToString(filter_input(INPUT_POST, 'body')) );

//送信ボタンが押された場合の処理
if (isset($_POST['submitted'])) {

  //POSTされたデータをチェック
  $_POST = checkInput( $_POST );

  //エラーメッセージを保存する配列の初期化
  $error = array();

  //値の検証
  if ( $name == '' ) {
    $error['name'] = '*お名前は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,30}\z/u', $name ) == 0 ) {
    $error['name'] = '*お名前は30文字以内でお願いします。';
  }
  if ( $email == '' ) {
    $error['email'] = '*メールアドレスは必須です。';
  } else { //メールアドレスを正規表現でチェック
    $pattern = '/\A([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}\z/uiD';
    if ( !preg_match( $pattern, $email ) ) {
      $error['email'] = '*メールアドレスの形式が正しくありません。';
    }
  }
  if ( $tel != '' && preg_match( '/\A\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}\z/u', $tel ) == 0 ) {
    $error['tel'] = '*電話番号の形式が正しくありません。';
  }
  if ( $subject == '' ) {
    $error['subject'] = '*件名は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,50}\z/u', $subject ) == 0 ) {
    $error['subject'] = '*件名は50文字以内でお願いします。';
  }
  if ( $body == '' ) {
    $error['body'] = '*内容は必須項目です。';
    //制御文字(タブ、復帰、改行を除く)でないことと文字数をチェック
  } else if ( preg_match( '/\A[\r\n\t[:^cntrl:]]{1,300}\z/u', $body ) == 0 ) {
    $error['body'] = '*内容は300文字以内でお願いします。';
  }

  //エラーがなく且つ POST でのリクエストの場合
  if (empty($error) && $_SERVER['REQUEST_METHOD']==='POST') {
    //メールアドレス等を記述したファイルの読み込み
    require '../libs/mailvars.php';

    //メール本文の組み立て
    $mail_body = 'コンタクトページからのお問い合わせ' . "\n\n";
    $mail_body .=  "お名前: " .h($name) . "\n";
    $mail_body .=  "Email: " . h($email) . "\n"  ;
    $mail_body .=  "お電話番号: " . h($tel) . "\n\n" ;
    $mail_body .=  "<お問い合わせ内容>" . "\n" . h($body);

    //--------sendmail------------

    //メールの宛先(名前<メールアドレス> の形式)。値は mailvars.php に記載
    $mailTo = mb_encode_mimeheader(MAIL_TO_NAME) ."<" . MAIL_TO. ">";

    //Return-Pathに指定するメールアドレス
    $returnMail = MAIL_RETURN_PATH; //
    //mbstringの日本語設定
    mb_language( 'ja' );
    mb_internal_encoding( 'UTF-8' );

    // 送信者情報(From ヘッダー)の設定
    $header = "From: " . mb_encode_mimeheader($name) ."<" . $email. ">\n";
    $header .= "Cc: " . mb_encode_mimeheader(MAIL_CC_NAME) ."<" . MAIL_CC.">\n";
    $header .= "Bcc: <" . MAIL_BCC.">";

    //メールの送信
    //メールの送信結果を変数に代入
    if ( ini_get( 'safe_mode' ) ) {
      //セーフモードがOnの場合は第5引数が使えない
      $result = mb_send_mail( $mailTo, $subject, $mail_body, $header );
    } else {
      $result = mb_send_mail( $mailTo, $subject, $mail_body, $header, '-f' . $returnMail );
    }

    //メール送信の結果判定
    if ( $result ) {
      $_POST = array(); //空の配列を代入し、すべてのPOST変数を消去
      //変数の値も初期化
      $name = '';
      $email = '';
      $tel = '';
      $subject = '';
      $body = '';

      //再読み込みによる二重送信の防止
      $params = '?result='. $result;
      //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
      if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
        $_SERVER['HTTPS'] = 'on';
      }
      $url = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://').$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];
      header('Location:' . $url . $params);
      exit;
    }
  }
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>コンタクトフォーム</title>
<link href="../bootstrap4/css/bootstrap.min.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="container">
  <h2 class="">お問い合わせフォーム</h2>
  <?php  if (filter_input(INPUT_GET, 'result') ) : // 送信が成功した場合?>
  <h4>送信完了!</h4>
  <p>送信完了いたしました。</p>
  <hr>
  <?php elseif (isset($result) && !$result ): // 送信が失敗した場合 ?>
  <h4>送信失敗</h4>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>メール:<a href="mailto:info@example.com">Contact</a></p>
  <hr>
  <?php endif; ?>
  <p>以下のフォームからお問い合わせください。</p>
  <form id="form" method="post">
    <div class="form-group">
      <label for="name">お名前(必須)
        <span class="error-php"><?php if ( isset( $error['name'] ) ) echo h( $error['name'] ); ?></span>
      </label>
      <input type="text" class="form-control" id="name" name="name" placeholder="氏名" required value="<?php echo h($name); ?>">
    </div>
    <div class="form-group">
      <label for="email">Email(必須)
        <span class="error-php"><?php if ( isset( $error['email'] ) ) echo h( $error['email'] ); ?></span>
      </label>
      <input type="email" class="form-control" id="email" name="email" pattern="([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}" placeholder="Email アドレス" required value="<?php echo h($email); ?>">
    </div>
    <div class="form-group">
      <label for="tel">お電話番号(半角英数字)
        <span class="error-php"><?php if ( isset( $error['tel'] ) ) echo h( $error['tel'] ); ?></span>
      </label>
      <input type="tel" class="form-control" id="tel" name="tel" pattern="\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}" value="<?php echo h($tel); ?>" placeholder="電話番号">
    </div>
    <div class="form-group">
       <label for="subject">件名(必須)
        <span class="error-php"><?php if ( isset( $error['subject'] ) ) echo h( $error['subject'] ); ?></span>
      </label>
      <input type="text" class="form-control" id="subject" name="subject" placeholder="件名" required maxlength="50" value="<?php echo h($subject); ?>">
    </div>
    <div class="form-group">
       <label for="body">お問い合わせ内容(必須)
        <span class="error-php"><?php if ( isset( $error['body'] ) ) echo h( $error['body'] ); ?></span>
      </label>
      <textarea class="form-control" id="body" name="body" placeholder="お問い合わせ内容" required  maxlength="300" rows="3"><?php echo h($body); ?></textarea>
    </div>
    <button name="submitted" type="submit" class="btn btn-primary">送信</button>
  </form>
</div>
</body>
</html>

PHP の部分では、値を検証する関数やエスケープ処理をする関数を記述した functions.php やメールの送信先の情報を記述した mailvars.php を require で読み込んでいます(3行目と61行目)。

この例では libs というフォルダを作成して保存しています。

├── contact
│   ├── contact.php
│   └── style.css
└── libs
    ├── .htaccesss
    ├── functions.php
    └── mailvars.php

libs フォルダには外部からアクセスできないように以下のような .htaccesss を配置しています。

.htaccess
deny from all
functions.php
<?php
//エスケープ処理を行う関数
function h($var) {
  if(is_array($var)){
    //$varが配列の場合、h()関数をそれぞれの要素について呼び出す(再帰)
    return array_map('h', $var);
  }else{
    if($var === null) return ''; // PHP 8.1.x 対策(null を渡すと Deprecated エラー)
    return htmlspecialchars($var, ENT_QUOTES, 'UTF-8');
  }
}

//入力値に不正なデータがないかなどをチェックする関数
function checkInput($var){
  if(is_array($var)){
    return array_map('checkInput', $var);
  }else{
    //NULLバイト攻撃対策
    if(preg_match('/\0/', $var)){
      die('不正な入力です。');
    }
    //文字エンコードのチェック
    if(!mb_check_encoding($var, 'UTF-8')){
      die('不正な入力です。');
    }
    //改行、タブ以外の制御文字のチェック
    if(preg_match('/\A[\r\n\t[:^cntrl:]]*\z/u', $var) === 0){
      die('不正な入力です。制御文字は使用できません。');
    }
    return $var;
  }
}
mailvars.php
<?php
//メールの宛先(To)のメールアドレス
define('MAIL_TO', "info@xxxxx.com");
//メールの宛先(To)の名前
define('MAIL_TO_NAME', "宛先の名前 ");
//Cc の名前
define('MAIL_CC', 'xxxx@xxxxxx.com');
//Cc の名前
define('MAIL_CC_NAME', 'Cc宛先名');
//Bcc
define('MAIL_BCC', 'xxxxx@xxxxx.com');
//Return-Pathに指定するメールアドレス
define('MAIL_RETURN_PATH', 'info@xxxxxx.com');
//自動返信の返信先名前(自動返信を設定する場合)
define('AUTO_REPLY_NAME', '返信先名前');

また、以下の例では Bootstrap4 の CSS を読み込んで以下のようなスタイルを指定しています。

/* input 要素 */
#name, #email, #subject, #email_check, #tel {
  max-width:400px;
}
#body {
  max-width: 640px;
}
/* エラー表示 */
.error, .error-php, .error-js {
  color: red;
}
/* フォーム要素(Bootstrap4 のスタイルを上書き) */
.form-control {
  border-radius: 0px;
  background-color: #fdfdfd;
  font-size: 14px;
}
.form-control:focus {
  border-color: #aadbe8;
  outline: 0;
  -webkit-box-shadow: inset 0 0px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.4);
  box-shadow: inset 0 0px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.4);
  background-color:#fff;
}
/* Google Chrome, Safari, Opera 15+, Android, iOS */
::-webkit-input-placeholder {
  font-size: 13px;
}
/* Firefox 18- */
:-moz-placeholder {
  font-size: 13px; }
/* Firefox 19+ */
::-moz-placeholder {
  font-size: 13px; }
/* IE 10+ */
:-ms-input-placeholder {
  font-size: 13px; }
::placeholder{
  font-size: 13px;
}
textarea.form-control {
  height: 200px;
}
HTML

フォームの送信先はこのファイル自信なので、form 要素の action 属性は省略しています。またメソッドは POST メソッドを指定しています(16行目)。

4〜14行目は送信ボタンがクリックされてメール送信を実行した結果を表示します。

この例では再読み込みによる二重送信を防止するため、送信が成功した場合は自分自身のページへリダイレクトしますがその際に、GET メソッドで送信結果を表す変数($result)の値が送られます。また、送信が失敗した場合は送信結果を表す変数 $result に false が入っています(詳細:二重送信防止)。

そのため、4行目では filter_input() を使って GET メソッドで送信された値(result)を取得して、その値が定義されていて値が真であれば、「送信完了!」と表示しています。そして8行目で送信結果を表す変数 $result を判定し、false であれば「送信失敗」と表示します。

4行目の filter_input() を使った記述は以下とほぼ同じことですが、PHP5.2 以降では filter_input() を使って $_POST や $_GET などの外部からの変数を取得した方が安全性が高まります。

 if ( isset($_GET['result']) && $_GET['result'] ) :

input 要素などフォームコントロールのクラス属性 class="form-control" は Bootstrap を使ったスタイルのための指定です。

HTML5 のフォームの検証機能

input 要素には入力する内容によって検証属性(required や pattern 等)や type 属性を指定して入力時の検証をしています(詳細:HTML5 のフォームの検証機能)。

必須項目には required 属性を指定し、文字数の制限は maxlength 属性を指定しています(指定した文字数以上は入力できなくなります)。Email は type 属性に email を指定すればメールアドレスの検証をしますが、PHP 側の検証と合わせるために、pattern 属性で独自の検証も追加しています。

  • 名前:required 属性、 maxlength 属性
  • Email:type 属性(email)、pattern 属性、required 属性
  • 電話番号:pattern 属性(適切な桁数の数値とカッコやハイフン、ドットを許容)
  • 件名:required 属性、 maxlength 属性
  • 問い合わせ内容:required 属性、 maxlength 属性

※ HTML5 でのクライアントサイドの検証は補助的なもので、PHP を使った検証が必要です。

サーバーサイド検証エラーの表示

適切に設定された HTML5 の検証を通過すれば、サーバー側(PHP)の検証を通過すると思いますが、HTML5 の検証が機能していない場合に不適切な値が入力された場合などサーバー側の検証を通過しない場合は、送信処理は行わずエラーを表示します。

PHP の検証でエラーがあった場合は、フォームの入力項目の label 要素の部分(error-php クラスを指定した span 要素)に出力しています。

例えば名前の値にエラーがあれば PHP の処理で $error['name'] が設定されるので、以下のようにエラーの内容をエスケープ処理して表示しています。

<?php if ( isset( $error['name'] ) ) echo h( $error['name'] ); ?>
contact.php HTML 部分抜粋
<body>
<div class="container">
  <h2 class="">お問い合わせフォーム</h2>
  <?php if (filter_input(INPUT_GET, 'result') ) : // 送信が成功した場合?>
  <h4>送信完了!</h4>
  <p>送信完了いたしました。</p>
  <hr>
  <?php elseif(isset($result) && !$result): // 送信が失敗した場合 ?>
  <h4>送信失敗</h4>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>メール:<a href="mailto:info@example.com">Contact</a></p>
  <hr>
  <?php endif; ?>
  <p>以下のフォームからお問い合わせください。</p>
  <form id="form" method="post">
    <div class="form-group">
      <label for="name">お名前(必須)
        <span class="error-php"><?php if(isset($error['name'])) echo h($error['name']); ?></span>
      </label>
      <input type="text" class="form-control" id="name" name="name" placeholder="氏名" required maxlength="30" value="<?php echo h($name); ?>">
    </div>
    <div class="form-group">
      <label for="email">Email(必須)
        <span class="error-php"><?php if(isset($error['email'])) echo h($error['email']); ?></span>
      </label>
      <input type="email" class="form-control" id="email" name="email" pattern="([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}" placeholder="Email アドレス" required value="<?php echo h($email); ?>">
    </div>
    <div class="form-group">
      <label for="tel">お電話番号(半角英数字)
        <span class="error-php"><?php if(isset($error['tel'])) echo h($error['tel']); ?></span>
      </label>
      <input type="tel" class="form-control" id="tel" name="tel" pattern="\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}" value="<?php echo h($tel); ?>" placeholder="電話番号">
    </div>
    <div class="form-group">
       <label for="subject">件名(必須)
        <span class="error-php"><?php if(isset($error['subject'])) echo h($error['subject']); ?></span>
      </label>
      <input type="text" class="form-control" id="subject" name="subject" placeholder="件名" required maxlength="50" value="<?php echo h($subject); ?>">
    </div>
    <div class="form-group">
       <label for="body">お問い合わせ内容(必須)
        <span class="error-php"><?php if(isset($error['body'])) echo h $error['body']); ?></span>
      </label>
      <textarea class="form-control" id="body" name="body" placeholder="お問い合わせ内容" required maxlength="300" rows="3"><?php echo h($body); ?></textarea>
    </div>
    <button name="submitted" type="submit" class="btn btn-primary">送信</button>
    <p style="margin-top: 30px;">※サンプルですので実際にはメールは送信されません</p>
  </form>
</div>
</body>
PHP

以下のコードの 12〜16行目では POST メソッドで取得するデータを初期化及び整形しています。

以下の場合、filter_input() の第1引数に INPUT_POST を、第2引数に name を指定しているので、POST メソッドで送信された name という変数がが存在すればその値を、存在しない場合は NULL を返します。

PHP8.1 からは null を trim() に渡すと、Deprecated エラーになるので、null を空文字列に変換する関数 nullToString を用意して、null の場合は空文字列に変換しています。

返された値の前後にあるホワイトスペースを trim() で削除して整形しています。

$name = trim( nullToString(filter_input(INPUT_POST, 'name')) );

filter_input() を使わずにほぼ同じことを isset() を使って以下のように記述できますが、filter_input() を使った方が安全です(外部からの変数を扱う)。

$name = trim( isset( $_POST[ 'name' ] ) ? $_POST[ 'name' ] : '' );

送信ボタン(name="submitted")がクリックされてフォームに入力されたデータが送信されると、$_POST['submitted']に値が設定されるので、19行目以降の処理が実行されます。

22行目は functions.php に記述してある関数 checkInput() で POST メソッドで送信された全ての値(配列)に不正なデータがないかなどをチェックして、不正なデータがあれば処理を終了します。

28行目以降の検証でエラーがあった場合には、25行目で初期化した配列 $error に出力するエラーメッセージを保存します。

28〜56行目では正規表現(preg_match)などを使って入力された値を検証しています。

関連ページPHP フォームの検証(バリデーション)

60行目は、再読み込みによる二重送信防止のため、リクエストが POST かどうかの判定とエラーがないかを判定しています。

65〜69行目では送信するメールの本文を生成しています。フォームから受け取った値は、念の為 functions.php に記述してある h() 関数でエスケープ処理しています。

74〜94行目は mb_send_mail() を使ったメールの送信処理です。

mb_send_mail() は成功した場合に TRUE を、失敗した場合に FALSE を返すので、その値を変数 $result に代入して、フォームが送信されてページが再読み込みされた際に、HTML ではその値をもとにメール送信の結果を表示します。

関連ページHTTP/パスワードハッシュ/メール操作(mb_send_mail())

問題なくメールが送信されれば、全ての $_POST 及び変数の値を消去しています(90〜97行目)。送信に失敗した場合は、それらの値は残しておいて再入力する際に利用できるようにしています。

PHP の検証機能の確認

PHP の検証機能の確認は、form 要素に novalidate 属性を指定して HMTL5 の検証を無効にして確認することができます。

contact.php PHP 部分抜粋
<?php
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';

// 値が null であれば、空文字列に変換する関数(trim() に null を渡すとエラーになるので)
function nullToString($val) {
  if($val === null) return '';
  return $val;
}

//POSTされたデータを変数に格納(値の初期化とデータの整形:前後にあるホワイトスペースを削除)
$name = trim( nullToString(filter_input(INPUT_POST, 'name')) );
$email = trim( nullToString(filter_input(INPUT_POST, 'email')) );
$tel = trim( nullToString(filter_input(INPUT_POST, 'tel')) );
$subject = trim( nullToString(filter_input(INPUT_POST, 'subject')) );
$body = trim( nullToString(filter_input(INPUT_POST, 'body')) );

//送信ボタンが押された場合の処理
if (isset($_POST['submitted'])) {

  //POSTされたデータをチェック
  $_POST = checkInput( $_POST );

  //エラーメッセージを保存する配列の初期化
  $error = array();

  //値の検証
  if ( $name == '' ) {
    //必須項目は値が空でないかを検証
    $error['name'] = '*お名前は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,30}\z/u', $name ) == 0 ) {
    $error['name'] = '*お名前は30文字以内でお願いします。';
  }
  if ( $email == '' ) {
    $error['email'] = '*メールアドレスは必須です。';
  } else { //メールアドレスを正規表現でチェック
    $pattern = '/\A([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}\z/uiD';
    if ( !preg_match( $pattern, $email ) ) {
      $error['email'] = '*メールアドレスの形式が正しくありません。';
    }
  }
  if ( $tel != '' && preg_match( '/\A\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}\z/u', $tel ) == 0 ) {
    $error['tel'] = '*電話番号の形式が正しくありません。';
  }
  if ( $subject == '' ) {
    $error['subject'] = '*件名は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,50}\z/u', $subject ) == 0 ) {
    $error['subject'] = '*件名は50文字以内でお願いします。';
  }
  if ( $body == '' ) {
    $error['body'] = '*内容は必須項目です。';
    //制御文字(タブ、復帰、改行を除く)でないことと文字数をチェック
  } else if ( preg_match( '/\A[\r\n\t[:^cntrl:]]{1,300}\z/u', $body ) == 0 ) {
    $error['body'] = '*内容は300文字以内でお願いします。';
  }

  //エラーがなく且つ POST でのリクエストの場合
  if (empty($error) && $_SERVER['REQUEST_METHOD']==='POST') {
    //メールアドレス等を記述したファイルの読み込み
    require '../libs/mailvars.php';

    //メール本文の組み立て
    $mail_body = 'コンタクトページからのお問い合わせ' . "\n\n";
    $mail_body .=  "お名前: " .h($name) . "\n";
    $mail_body .=  "Email: " . h($email) . "\n"  ;
    $mail_body .=  "お電話番号: " . h($tel) . "\n\n" ;
    $mail_body .=  "<お問い合わせ内容>" . "\n" . h($body);

    //--------sendmail------------

    //メールの宛先(名前<メールアドレス> の形式)。値は mailvars.php に記載
    $mailTo = mb_encode_mimeheader(MAIL_TO_NAME) ."<" . MAIL_TO. ">";

    //Return-Pathに指定するメールアドレス
    $returnMail = MAIL_RETURN_PATH; //
    //mbstringの日本語設定
    mb_language( 'ja' );
    mb_internal_encoding( 'UTF-8' );

    // 送信者情報(From ヘッダー)の設定
    $header = "From: " . mb_encode_mimeheader($name) ."<" . $email. ">\n";
    $header .= "Cc: " . mb_encode_mimeheader(MAIL_CC_NAME) ."<" . MAIL_CC.">\n";
    $header .= "Bcc: <" . MAIL_BCC.">";

    //メールの送信
    //メールの送信結果を変数に代入
    if ( ini_get( 'safe_mode' ) ) {
      //セーフモードがOnの場合は第5引数が使えない
      $result = mb_send_mail( $mailTo, $subject, $mail_body, $header );
    } else {
      $result = mb_send_mail( $mailTo, $subject, $mail_body, $header, '-f' . $returnMail );
    }

    //メール送信の結果判定
    if ( $result ) {
      $_POST = array(); //空の配列を代入し、すべてのPOST変数を消去
      //変数の値も初期化
      $name = '';
      $email = '';
      $tel = '';
      $subject = '';
      $body = '';

      //再読み込みによる二重送信の防止
      $params = '?result='. $result;
      //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
      if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
        $_SERVER['HTTPS'] = 'on';
      }
      $url = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://').$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];
      header('Location:' . $url . $params);
      exit;
    }
  }
}

PHP でのフォームの検証方法などについては以下を御覧ください。

フォームの検証(バリデーション)

二重送信防止

送信ボタンをクリックして送信完了後に、ページを再読み込みすると「フォーム再送信の確認」メッセージが出て「続行」をクリックするとフォームの入力欄は空ですが、同じ内容が二重に送信されてしまいます。

これを回避する1つの方法は、送信完了後に header() 関数で Location ヘッダを使用して自分自身のページへリダイレクトさせます。

header() 関数は HTTP ヘッダを送信するための関数で、Location ヘッダに指定した URL にジャンプ(移動)させることができ、これは GET リクエストが使われます。

フォームの送信には POST リクエストを使うので、$_SERVER['REQUEST_METHOD']を使って POST でのリクエストの場合のみメールを送信するようにします。

そしてメール送信完了後(送信が成功した場合)に header() 関数で自分自身へリダイレクトします。 自分自身の URL は $_SERVER(環境変数)を使って組み立てています。

その際に、送信結果を表示するために必要な値 $result(送信結果の値)を GET リクエストのパラメータに付加します($params = '?result='. $result)。

//POST でのリクエストの場合のみ以下を実行
if ($_SERVER['REQUEST_METHOD']==='POST') {

・・・メールの送信処理・・・

//メール送信が成功した場合の処理
if ( $result ) {

  ・・・

  //GET リクエストに追加するパラメータ
  $params = '?result='. $result;
  //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
  if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
    $_SERVER['HTTPS'] = 'on';
  }
  //自分自身の URL
  $url = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://').$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];
  header('Location:' . $url . $params);
  exit;
}

14〜16行目はサーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用の記述です(サーバーによっては https での通信でも $_SERVER['HTTPS'] の値が空になるため)。

常時SSL化している場合は、14〜18行目は以下のように記述することもできます。SSL化していない場合は https を http に変更します。

$url = 'https://'.$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];

HTML では送信が成功した場合は $_GET['result'] に 1(true)が入っているので、「送信完了!」と表示します。

送信が失敗した場合は、リダイレクトはされず $result に false が入っているので「送信失敗」と表示します。

<?php if( filter_input(INPUT_GET, 'result') ) : // 送信が成功した場合?>
<h4>送信完了!</h4>
<p>送信完了いたしました。</p>
<hr>
<?php elseif (isset($result) && !$result ): // 送信が失敗した場合 ?>
<h4>送信失敗</h4>
<p>申し訳ございませんが、送信に失敗しました。</p>
<p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
<p>メール:<a href="mailto:info@example.com">Contact</a></p>
<hr>
<?php endif; ?>
完了ページを表示

送信が完了(成功)した場合には、別途用意した完了ページを表示する例です。

サンプルを別ページで開く

この例では以下のようなシンプルな完了ページ(complete.php)を別途用意します。実際にはご案内やリンク、画像などを表示することが考えられます。

complete.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>完了画面</title>
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="container">
  <h1 class="">完了画面</h1>
  <h2>送信完了いたしました。</h2>
  <p>ありがとうございました。</p>
</div>
</body>
</html>

コンタクトフォームの PHP の部分は、前述の例とほぼ同じですが、送信が成功した場合の二重送信防止の部分で自分自身へのリダイレクトを完了ページへのリダイレクトに変更します。

contact1.php PHP 抜粋
//メールが送信された場合の処理
if ( $result ) {
  //空の配列を代入し、すべてのPOST変数を消去
  $_POST = array();

  //変数の値も初期化
  $name = '';
  $email = '';
  $tel = '';
  $subject = '';
  $body = '';

  //完了ページ(complete.php)へリダイレクト
  $url = 'complete.php';
  header('Location:' . $url );
  exit;
} 

HTML 部分は送信が成功した場合の部分が不要になります。

contact1.php HTML 抜粋
<div class="container">
  <h2 class="">お問い合わせフォーム</h2>
  <?php if (isset($result) && !$result ): // 送信が失敗した場合 ?>
  <h4>送信失敗</h4>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>メール:<a href="mailto:info@example.com">Contact</a></p>
  <hr>
  <?php endif; ?>
  <p>以下のフォームからお問い合わせください。</p>
  <form id="form" method="post">
  ・・・以下省略・・・
  
完了ページに入力内容を表示

以下は完了ページにフォームへの入力内容を表示する例です。

但し、完了ページへのリダイレクトは GET メソッドを使用するので、入力内容が URL に表示されてしまうので、内容によりこの方法を使うかどうかを検討する必要があります。

サンプルを別ページで開く

前述の例との違いは、送信が成功した場合の処理で、変数の値を初期化する前にそれぞれの値をパラメータ用の変数($params)に格納します。

[注意]これらの情報はパラメータとして URL に表示されます。

以下では全ての情報をパラメータに渡していますが、例えば名前や電話番号、メールアドレスは除外するなどを検討(配慮)する必要があります。

//メールが送信された場合の処理
if ( $result ) {
  //空の配列を代入し、すべてのPOST変数を消去
  $_POST = array();

  //リダイレクトの URL に付加するパラメータ用の変数
  $params = '?';
  $params .= 'name='. h($name);
  $params .= '&email='. h($email);
  $params .= '&tel='. h($tel);
  $params .= '&subject='. h($subject);
  $params .= '&body='. h($body);

  //変数の値も初期化
  $name = '';
  $email = '';
  $tel = '';
  $subject = '';
  $body = '';

  //完了画面の URL にパラメータを付加してリダイレクト
  $url = 'complete_2.php';
  header('Location:' . $url . $params);
  exit;
} 
<?php
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';

// 値が null であれば、空文字列に変換する関数
function nullToString($val) {
  if($val === null) return '';
  return $val;
}

//POSTされたデータの前後にあるホワイトスペースを削除してを変数に格納
$name = trim( nullToString(filter_input(INPUT_POST, 'name')) );
$email = trim( nullToString(filter_input(INPUT_POST, 'email')) );
$tel = trim( nullToString(filter_input(INPUT_POST, 'tel')) );
$subject = trim( nullToString(filter_input(INPUT_POST, 'subject')) );
$body = trim( nullToString(filter_input(INPUT_POST, 'body')) );

//送信ボタンが押された場合の処理
if (isset($_POST['submitted'])) {

  //POSTされたデータをチェック
  $_POST = checkInput( $_POST );

  //エラーメッセージを保存する配列の初期化
  $error = array();

  //値の検証
  if ( $name == '' ) {
    $error['name'] = '*お名前は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,30}\z/u', $name ) == 0 ) {
    $error['name'] = '*お名前は30文字以内でお願いします。';
  }
  if ( $email == '' ) {
    $error['email'] = '*メールアドレスは必須です。';
  } else { //メールアドレスを正規表現でチェック
    $pattern = '/\A([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}\z/uiD';
    if ( !preg_match( $pattern, $email ) ) {
      $error['email'] = '*メールアドレスの形式が正しくありません。';
    }
  }
  if ( $tel != '' && preg_match( '/\A\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}\z/u', $tel ) == 0 ) {
    $error['tel'] = '*電話番号の形式が正しくありません。';
  }
  if ( $subject == '' ) {
    $error['subject'] = '*件名は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,50}\z/u', $subject ) == 0 ) {
    $error['subject'] = '*件名は50文字以内でお願いします。';
  }
  if ( $body == '' ) {
    $error['body'] = '*内容は必須項目です。';
    //制御文字(タブ、復帰、改行を除く)でないことと文字数をチェック
  } else if ( preg_match( '/\A[\r\n\t[:^cntrl:]]{1,300}\z/u', $body ) == 0 ) {
    $error['body'] = '*内容は300文字以内でお願いします。';
  }

  //エラーがなく且つ POST でのリクエストの場合
  if (empty($error) && $_SERVER['REQUEST_METHOD']==='POST') {
    //メールアドレス等を記述したファイルの読み込み
    require '../libs/mailvars.php';

    //メール本文の組み立て
    $mail_body = 'コンタクトページからのお問い合わせ' . "\n\n";
    $mail_body .=  "お名前: " .h($name) . "\n";
    $mail_body .=  "Email: " . h($email) . "\n"  ;
    $mail_body .=  "お電話番号: " . h($tel) . "\n\n" ;
    $mail_body .=  "<お問い合わせ内容>" . "\n" . h($body);

    //--------sendmail------------

    //メールの宛先(名前<メールアドレス> の形式)。値は mailvars.php に記載
    $mailTo = mb_encode_mimeheader(MAIL_TO_NAME) ."<" . MAIL_TO. ">";

    //Return-Pathに指定するメールアドレス
    $returnMail = MAIL_RETURN_PATH; //
    //mbstringの日本語設定
    mb_language( 'ja' );
    mb_internal_encoding( 'UTF-8' );

    // 送信者情報(From ヘッダー)の設定
    $header = "From: " . mb_encode_mimeheader($name) ."<" . $email. ">\n";
    $header .= "Cc: " . mb_encode_mimeheader(MAIL_CC_NAME) ."<" . MAIL_CC.">\n";
    $header .= "Bcc: <" . MAIL_BCC.">";

    //メールの送信
    //メールの送信結果を変数に代入
    if ( ini_get( 'safe_mode' ) ) {
      //セーフモードがOnの場合は第5引数が使えない
      $result = mb_send_mail( $mailTo, $subject, $mail_body, $header );
    } else {
      $result = mb_send_mail( $mailTo, $subject, $mail_body, $header, '-f' . $returnMail );
    }

    //メールが送信された場合の処理
    if ( $result ) {
      //空の配列を代入し、すべてのPOST変数を消去
      $_POST = array();

      $params = '?';
      $params .= 'name='. h($name);
      $params .= '&email='. h($email);
      $params .= '&tel='. h($tel);
      $params .= '&subject='. h($subject);
      $params .= '&body='. h($body);

      //変数の値も初期化
      $name = '';
      $email = '';
      $tel = '';
      $subject = '';
      $body = '';

      //完了画面へリダイレクト
      $url = 'complete_2.php';
      header('Location:' . $url . $params);
      exit;
    }
  }
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>コンタクトフォーム</title>
<link href="../bootstrap4/css/bootstrap.min.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="container">
  <h2 class="">お問い合わせフォーム</h2>
  <?php if (isset($result) && !$result ): // 送信が失敗した場合 ?>
  <h4>送信失敗</h4>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>メール:<a href="mailto:info@example.com">Contact</a></p>
  <hr>
  <?php endif; ?>
  <p>以下のフォームからお問い合わせください。</p>
  <form id="form" method="post">
    <div class="form-group">
      <label for="name">お名前(必須)
        <span class="error-php"><?php if ( isset( $error['name'] ) ) echo h( $error['name'] ); ?></span>
      </label>
      <input type="text" class="form-control" id="name" name="name" placeholder="氏名" required maxlength="30" value="<?php echo h($name); ?>">
    </div>
    <div class="form-group">
      <label for="email">Email(必須)
        <span class="error-php"><?php if ( isset( $error['email'] ) ) echo h( $error['email'] ); ?></span>
      </label>
      <input type="email" class="form-control" id="email" name="email" pattern="([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}" placeholder="Email アドレス" required value="<?php echo h($email); ?>">
    </div>
    <div class="form-group">
      <label for="tel">お電話番号(半角英数字)
        <span class="error-php"><?php if ( isset( $error['tel'] ) ) echo h( $error['tel'] ); ?></span>
      </label>
      <input type="tel" class="form-control" id="tel" name="tel" pattern="\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}" value="<?php echo h($tel); ?>" placeholder="電話番号">
    </div>
    <div class="form-group">
       <label for="subject">件名(必須)
        <span class="error-php"><?php if ( isset( $error['subject'] ) ) echo h( $error['subject'] ); ?></span>
      </label>
      <input type="text" class="form-control" id="subject" name="subject" placeholder="件名" required maxlength="50" value="<?php echo h($subject); ?>">
    </div>
    <div class="form-group">
       <label for="body">お問い合わせ内容(必須)
        <span class="error-php"><?php if ( isset( $error['body'] ) ) echo h( $error['body'] ); ?></span>
      </label>
      <textarea class="form-control" id="body" name="body" placeholder="お問い合わせ内容" required maxlength="300" rows="3"><?php echo h($body); ?></textarea>
    </div>
    <button name="submitted" type="submit" class="btn btn-primary">送信</button>
  </form>
</div>
</body>
</html>

完了ページでは、filter_input() でパラメータ(INPUT_GET)から値を取得して表示します。

complete_2.php
<?php
//エスケープ処理を行う関数
function h($var) {
  if($var === null) return '';
  return htmlspecialchars($var, ENT_QUOTES, 'UTF-8');
}
//GET メソッドで渡された値を初期化(取得)
$name = filter_input(INPUT_GET, 'name');
$email = filter_input(INPUT_GET, 'email');
$tel = filter_input(INPUT_GET, 'tel');
$subject = filter_input(INPUT_GET, 'subject');
$body = filter_input(INPUT_GET, 'body');
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>完了画面</title>
<link href="../bootstrap4/css/bootstrap.min.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="container">
  <h2 class="">完了画面</h2>
  <h4>送信完了いたしました。</h4>
  <p>この度は、お問い合わせ頂き誠にありがとうございます。</p>
  <p>以下の内容でお問い合わせを受け付けました。</p>
  <div class="table-responsive">
    <table class="table">
      <tr>
        <th>お名前</th>
        <td><?php if($name) echo h($name); ?></td>
      </tr>
      <tr>
        <th>メールアドレス</th>
        <td><?php if($email ) echo h($email); ?></td>
      </tr>
      <tr>
        <th>お電話番号</th>
        <td><?php if($tel) echo h($tel); ?></td>
      </tr>
      <tr>
        <th>件名</th>
        <td><?php if($subject) echo h($subject); ?></td>
      </tr>
      <tr>
        <th>お問い合わせ内容</th>
        <td><?php if($body) echo h($body); ?></td>
      </tr>
    </table>
  </div>
</div>
</body>
</html>

JavaScript で検証

HTML5 の検証機能を使った場合、表示されるエラーやその見栄えがブラウザごとに異なっていたり、サポートしていないブラウザ(IE9 以下など caniuse.com)では検証がされません。

前述の例の場合、form 要素に novalidate 属性を指定してHMTL5 の検証を無効にすれば、PHP のみの検証になり、エラーやその見栄えは調整できます。

但し、PHP の検証の場合、データを一度サーバに送信してから処理されるため、環境によっては検証結果の表示に時間がかかるなど、使い勝手が良くありません。

JavaScript を使って検証すれば、データの送信前に入力のエラーなどをユーザに伝えることができます。

JavaScript の検証では PHP によるサーバー側と同等の検証を行います(JavaScript の検証を通過すれば、PHP の検証も通過するように)。

また、JavaScript の検証は JavaScript がオフになっていると機能しないので、JavaScript の検証を実装しても PHP の検証も必ず実装する必要があります。

以下は JavaScript の検証を使ったサンプルです。

サンプルを別ページで開く

HTML

前述の例とほぼ同じ内容のフォームですが、この例では確認用の E-mail の入力欄を追加しています。

この例では JavaScript を使って検証するために form 要素に validationForm というクラス属性を、検証対象のフォームコントロールに以下のようなクラス属性を指定します。

クラス 意味 指定が必要な属性
required 必須入力 なし
maxlength 最大文字数 data-maxlength:最大文字数を指定
minlength 最小文字数 data-minlength:最小文字数を指定
pattern パターン検証 data-pattern:検証に使うパターンを指定(Email の検証は email、電話番号の検証は tel と指定可能)
equal-to 値が一致するかを検証 data-equal-to:値を比較する要素の id 属性を指定
showCount 入力された文字数を出力 なし

data-error-xxxx 属性(xxxx は上記クラス名)に独自のエラーメッセージを指定して表示できます。この属性を指定しない場合は、デフォルトのエラーメッセージを表示します。

また、この例では HTML5 の自動検証が行われないように form 要素に novalidate 属性を指定しています(HTML5 の required などの検証属性を指定していない場合でも、type 属性に email を指定すると HTML5 の検証が行われます)。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>コンタクトフォーム</title>
<link href="../bootstrap4/css/bootstrap.min.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="container">
  <h2 class="">お問い合わせフォーム</h2>
  <?php if (filter_input(INPUT_GET, 'result') ) : // 送信が成功した場合?>
  <h4>送信完了!</h4>
  <p>送信完了いたしました。</p>
  <hr>
  <?php elseif (isset($result) && !$result ): // 送信が失敗した場合 ?>
  <h4>送信失敗</h4>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>メール:<a href="mailto:info@example.com">Contact</a></p>
  <hr>
  <?php endif; ?>
  <p>以下のフォームからお問い合わせください。</p>
  <form id="form" class="validationForm" method="post" novalidate>
    <div class="form-group">
      <label for="name">お名前(必須)
        <span class="error-php"><?php if(isset($error['name'])) echo h($error['name']); ?></span>
      </label>
      <input type="text" class="required maxlength form-control" data-maxlength="30" id="name" name="name" placeholder="氏名" data-error-required="お名前は必須です。" value="<?php echo h($name); ?>">
    </div>
    <div class="form-group">
      <label for="email">Email(必須)
        <span class="error-php"><?php if(isset($error['email'])) echo h($error['email']); ?></span>
      </label>
      <input type="email" class="required pattern form-control" data-pattern="email" id="email" name="email" placeholder="Email アドレス" data-error-required="Email アドレスは必須です。"  data-error-pattern="Email の形式が正しくないようですのでご確認ください" value="<?php echo h($email); ?>">
    </div>
    <div class="form-group">
      <label for="email_check">Email(確認用 必須)
        <span class="error-php"><?php if(isset($error['email_check'])) echo h($error['email_check']); ?></span>
      </label>
      <input type="email"  class="form-control equal-to required" data-equal-to="email" data-error-equal-to="メールアドレスが異なります" id="email_check" name="email_check" placeholder="Email アドレス(確認用 必須)" value="<?php echo h($email_check); ?>">
    </div>
    <div class="form-group">
      <label for="tel">お電話番号(半角英数字)
        <span class="error-php"><?php if(isset($error['tel'])) echo h($error['tel']); ?></span>
      </label>
      <input type="tel" class="pattern form-control" data-pattern="tel" id="tel" name="tel" placeholder="お電話番号" data-error-pattern="電話番号の形式が正しくないようですのでご確認ください"  value="<?php echo h($tel); ?>">
    </div>
    <div class="form-group">
      <label for="subject">件名(必須)
        <span class="error-php"><?php if(isset($error['subject'])) echo h($error['subject']); ?></span>
      </label>
      <input type="text" class="required maxlength form-control" data-maxlength="100" id="subject" name="subject" placeholder="件名" value="<?php echo h($subject); ?>">
    </div>
    <div class="form-group">
      <label for="body">お問い合わせ内容(必須)
        <span class="error-php"><?php if(isset($error['body'])) echo h($error['body']); ?></span>
      </label>
      <textarea class="required maxlength showCount form-control" data-maxlength="1000" id="body" name="body" placeholder="お問い合わせ内容(1000文字まで)をお書きください" rows="3"><?php echo h($body); ?></textarea>
    </div>
    <button name="submitted" type="submit" class="btn btn-primary">送信</button>
  </form>
</div>
<!--  検証用の JavaScript の読み込み(または script タグに検証用スクリプトを記述) -->
<script src="formValidation.js"></script>
</body>
</html>
PHP

PHP は確認用メールアドレスの値の変数($email_check:14行目)と2つのメールアドレスが一致しているかの検証(42〜48行目)を追加しただけで、その他は前述の例と同じです。

<?php
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';

// 値が null であれば、空文字列に変換する関数
function nullToString($val) {
  if($val === null) return '';
  return $val;
}

//POSTされたデータの前後にあるホワイトスペースを削除してを変数に格納
$name = trim( nullToString(filter_input(INPUT_POST, 'name')) );
$email = trim( nullToString(filter_input(INPUT_POST, 'email')) );
$email_check = trim( nullToString(filter_input(INPUT_POST, 'email_check')) );
$tel = trim( nullToString(filter_input(INPUT_POST, 'tel')) );
$subject = trim( nullToString(filter_input(INPUT_POST, 'subject')) );
$body = trim( nullToString(filter_input(INPUT_POST, 'body')) );

if (isset($_POST['submitted'])) {

  //POSTされたデータをチェック
  $_POST = checkInput( $_POST );

  //エラーメッセージを保存する配列の初期化
  $error = array();

  //値の検証
  if ( $name == '' ) {
    $error['name'] = '*お名前は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,30}\z/u', $name ) == 0 ) {
    $error['name'] = '*お名前は30文字以内でお願いします。';
  }
  if ( $email == '' ) {
    $error['email'] = '*メールアドレスは必須です。';
  } else { //メールアドレスを正規表現でチェック
    $pattern = '/\A([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}\z/uiD';
    if ( !preg_match( $pattern, $email ) ) {
      $error['email'] = '*メールアドレスの形式が正しくありません。';
    }
  }
  if ( $email_check == '' ) {
    $error['email_check'] = '*確認用メールアドレスは必須です。';
  } else { //メールアドレスを正規表現でチェック
    if ( $email_check !== $email ) {
      $error['email_check'] = '*メールアドレスが一致しません。';
    }
  }
  if ( $tel != '' && preg_match( '/\A\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}\z/u', $tel ) == 0 ) {
    $error['tel'] = '*電話番号の形式が正しくありません。';
  }
  if ( $subject == '' ) {
    $error['subject'] = '*件名は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,100}\z/u', $subject ) == 0 ) {
    $error['subject'] = '*件名は100文字以内でお願いします。';
  }
  if ( $body == '' ) {
    $error['body'] = '*内容は必須項目です。';
    //制御文字(タブ、復帰、改行を除く)でないことと文字数をチェック
  } else if ( preg_match( '/\A[\r\n\t[:^cntrl:]]{1,1050}\z/u', $body ) == 0 ) {
    $error['body'] = '*内容は1000文字以内でお願いします。';
  }

  //エラーがなく且つ POST でのリクエストの場合
  if (empty($error) && $_SERVER['REQUEST_METHOD']==='POST') {
    //メールアドレス等を記述したファイルの読み込み
    require '../libs/mailvars.php';

    //メール本文の組み立て
    $mail_body = 'コンタクトページからのお問い合わせ' . "\n\n";
    $mail_body .=  "お名前: " .h($name) . "\n";
    $mail_body .=  "Email: " . h($email) . "\n"  ;
    $mail_body .=  "お電話番号: " . h($tel) . "\n\n" ;
    $mail_body .=  "<お問い合わせ内容>" . "\n" . h($body);

    //--------sendmail------------

    //メールの宛先(名前<メールアドレス> の形式)。値は mailvars.php に記載
    $mailTo = mb_encode_mimeheader(MAIL_TO_NAME) ."<" . MAIL_TO. ">";

    //Return-Pathに指定するメールアドレス
    $returnMail = MAIL_RETURN_PATH; //
    //mbstringの日本語設定
    mb_language( 'ja' );
    mb_internal_encoding( 'UTF-8' );

    // 送信者情報(From ヘッダー)の設定
    $header = "From: " . mb_encode_mimeheader($name) ."<" . $email. ">\n";
    $header .= "Cc: " . mb_encode_mimeheader(MAIL_CC_NAME) ."<" . MAIL_CC.">\n";
    $header .= "Bcc: <" . MAIL_BCC.">";

    //メールの送信
    //メールの送信結果を変数に代入 (サンプルなのでコメントアウト)
    if ( ini_get( 'safe_mode' ) ) {
      //セーフモードがOnの場合は第5引数が使えない
      $result = mb_send_mail( $mailTo, $subject, $mail_body, $header );
    } else {
      $result = mb_send_mail( $mailTo, $subject, $mail_body, $header, '-f' . $returnMail );
    }

    //メール送信の結果判定
    if ( $result ) {
      $_POST = array(); //空の配列を代入し、すべてのPOST変数を消去
      //変数の値も初期化
      $name = '';
      $email = '';
      $email_check = '';
      $tel = '';
      $subject = '';
      $body = '';

      //再読み込みによる二重送信の防止
      $params = '?result='. $result;
      //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
      if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
        $_SERVER['HTTPS'] = 'on';
      }
      $url = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://').$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];
      header('Location:' . $url . $params);
      exit;
    }
  }
}
?>

PHP の検証の確認

このサンプルの場合、form 要素に指定している validationForm クラスを削除すると、JavaScript の検証が無効になるので PHP の検証を確認することができます。

JavaScript

この例の JavaScript では validationForm クラスを指定したフォーム(form 要素)の required や maxlength、pattern、equal-to という検証用クラスを指定した要素を対象に検証をします。

また、最大文字数を検証する maxlength クラス及び data-maxlength 属性を指定した要素に showCount クラスが指定されていれば入力文字数を表示するようにしています。

検証のタイミングは送信時及び初回の送信後(エラーがある場合) input イベントを使って入力された値が変更される度に検証するようにしています。

初回送信前の入力時にも検証エラーを表示するには4行目の validateAfterFirstSubmit を false に変更します。検証のエラーは error-js クラスを付与した span 要素を追加することで出力しますが、このクラス(error-js)を変更するには5行目のクラス名を変更します。

以下のスクリプトでは、それぞれの検証に使用する関数を定義し、フォームの送信時(submit イベント)及び対象の要素の値が変更される際(input イベント)に検証しています。検証の結果エラーがあれば、サーバーには送信せずエラーを表示して、最初のエラーの位置までスクロールするようにしています。

独自のエラーメッセージや検証パターンを設定できるようになっていたり、コントロールの種類や数が増えても機能するようになっているためスクリプトは長くなってしまっています。

JavaScript の内容はコメント付きのコードをご覧いただくか、詳細は JavaScript フォームの検証(制約検証 API を使わない方法) を御覧ください。

関連ページ:JavaScript を使ったフォームの検証(入力チェック)

//validationForm クラス と novalidate 属性を指定した form 要素を独自に検証
document.addEventListener('DOMContentLoaded', () => {
  //validationForm クラスを指定した最初の form 要素を取得
  const validationForm = document.getElementsByClassName('validationForm')[0];
  //初回送信前にはエラーを表示しない(送信時及び送信後にエラーがあればエラーを表示)
  let validateAfterFirstSubmit = true;
  //エラーを表示する span 要素に付与するクラス名
  const errorClassName = 'error-js';

  if(validationForm) {
    //required クラスを指定された要素の集まり
    const requiredElems = document.querySelectorAll('.required');
    //pattern クラスを指定された要素の集まり
    const patternElems =  document.querySelectorAll('.pattern');
    //equal-to クラスを指定された要素の集まり
    const equalToElems = document.querySelectorAll('.equal-to');
    //minlength クラスを指定された要素の集まり
    const minlengthElems =  document.querySelectorAll('.minlength');
    //maxlength クラスを指定された要素の集まり
    const maxlengthElems =  document.querySelectorAll('.maxlength');
    //showCount クラスを指定された要素の集まり
    const showCountElems =  document.querySelectorAll('.showCount');

    //エラーメッセージを表示する span 要素を生成して親要素に追加する関数
    //elem :対象の要素
    //className :エラーメッセージの要素に追加するクラス名
    //defaultMessage:デフォルトのエラーメッセージ
    const addError = (elem, className, defaultMessage) => {
      //戻り値として返す変数 errorMessage にデフォルトのエラーメッセージを代入
      let errorMessage = defaultMessage;
      //要素に data-error-xxxx 属性が指定されていれば(xxxx は第2引数の className)
      if(elem.hasAttribute('data-error-' + className)) {
        //data-error-xxxx  属性の値を取得
        const dataError = elem.getAttribute('data-error-' + className);
        //data-error-xxxx  属性の値が label であれば
        if(dataError) {// data-error-xxxx  属性の値が label 以外の場合
          //data-error-xxxx  属性の値をエラーメッセージとする
          errorMessage = dataError;
        }
      }
      //初回の送信前にはエラー表示はせず、送信時及び送信後の再入力時にエラーを表示
      if(!validateAfterFirstSubmit) {
        //span 要素を生成
        const errorSpan = document.createElement('span');
        //error 及び引数に指定されたクラスを追加(設定)
        errorSpan.classList.add(errorClassName, className);
        //aria-live 属性を設定
        errorSpan.setAttribute('aria-live', 'polite');
        //引数に指定されたエラーメッセージを設定
        errorSpan.textContent = errorMessage;
        //elem の親要素の子要素として追加
        elem.parentNode.appendChild(errorSpan);
      }
    }

    //値が空かどうかを検証及びエラーを表示する関数(空の場合は true を返す)
    //elem :対象の要素
    const isValueMissing = (elem) => {
      //ラジオボタンの場合
      if(elem.tagName === 'INPUT' && elem.getAttribute('type') === 'radio') {
        //エラーメッセージの要素に追加するクラス名(data-error-xxxx の xxxx)
        const className = 'required-radio';
        //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る)
        const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className);
        //選択状態の最初のラジオボタン要素を取得
        const checkedRadio = elem.parentElement.querySelector('input[type="radio"]:checked');
        //選択状態のラジオボタン要素を取得できない場合
        if(checkedRadio === null) {
         if(!errorSpan) {
           //addError() を使ってエラーメッセージ表示する span 要素を生成して追加
            addError(elem, className, '選択は必須です');
          }
          return true;
        } else{ //いずれかのラジオボタンが選択されている場合
          //エラーメッセージ表示する span 要素がすでに存在すれば削除してエラーをクリア
          if(errorSpan) {
            elem.parentNode.removeChild(errorSpan);
          }
          return false;
        }
      }else if(elem.tagName === 'INPUT' && elem.getAttribute('type') === 'checkbox') {
        //チェックボックスの場合
        const className = 'required-checkbox';
        const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className);
        //選択状態の最初のチェックボックス要素を取得
        const checkedCheckbox = elem.parentElement.querySelector('input[type="checkbox"]:checked');
        if(checkedCheckbox === null) {
          if(!errorSpan) {
            addError(elem, className, '選択は必須です');
          }
          return true;
        }else{
          if(errorSpan) {
            elem.parentNode.removeChild(errorSpan);
          }
          return false;
        }
      }else{
        //テキストフィールドやテキストエリア、セレクトボックスの場合
        const className = 'required';
        const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className);
        //値が空の場合はエラーを表示して true を返す(trim() で前後の空白文字を削除)
        if(elem.value.trim().length === 0) {
          if(!errorSpan) {
            if(elem.tagName === 'SELECT') {
              //セレクトボックスの場合
              addError(elem, className, '選択は必須です');
            }else{
              //テキストフィールドやテキストエリアの場合
              addError(elem, className, '入力は必須です');
            }
          }
          return true;
        }else{
          if(errorSpan) {
            elem.parentNode.removeChild(errorSpan);
          }
          return false;
        }
      }
    }

    //required クラスを指定された要素に input イベントを設定(値が変更される都度に検証)
    requiredElems.forEach( (elem) => {
      //ラジオボタンまたはチェックボックスの場合
      if(elem.tagName === 'INPUT' && (elem.getAttribute('type') === 'radio' || elem.getAttribute('type') === 'checkbox' )){
        //親要素を基点に全てのラジオボタンまたはチェックボックス要素を取得
        const elems = elem.parentElement.querySelectorAll(elem.tagName);
        //取得した全ての要素に change イベントを設定
        elems.forEach( (elemsChild) => {
          elemsChild.addEventListener('change', () => {
            //それぞれの要素の選択状態が変更されたら検証を実行
            isValueMissing(elemsChild);
          });
        });
      }else{
        elem.addEventListener('input', () => {
          //要素の値が変更されたら検証を実行
          isValueMissing(elem);
        });
      }
    });

    //指定されたパターンにマッチしているかを検証する関数(マッチしていない場合は true を返す)
    //elem :対象の要素
    const isPatternMismatch = (elem) => {
      //検証対象のクラス名
      const className = 'pattern';
      //対象の(パターンが記述されている) data-xxxx 属性(data-pattern)
      const attributeName = 'data-' + className;
      //data-pattern 属性にパターンが指定されていればその値をパターンとする
      let pattern = new RegExp('^' + elem.getAttribute(attributeName) + '$');
      //data-pattern 属性の値が email の場合
      if(elem.getAttribute(attributeName) ==='email') {
        pattern = /^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/ui;
      }else if(elem.getAttribute(attributeName) ==='tel') { //data-pattern 属性の値が tel の場合
        pattern = /^\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}$/;
      }
      //エラーを表示する span 要素がすでに存在すれば取得
      const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className);
      //対象の要素の値が空でなければパターンにマッチするかを検証
      if(elem.value.trim() !=='') {
        if(!pattern.test(elem.value)) {
          if(!errorSpan) {
            addError(elem, className, '入力された値が正しくないようです');
          }
          return true;
        }else{
          if(errorSpan) {
            elem.parentNode.removeChild(errorSpan);
          }
          return false;
        }
      }else if(elem.value ==='' && errorSpan) {
        elem.parentNode.removeChild(errorSpan);
      }
    }

    //pattern クラスを指定された要素に input イベントを設定(値が変更される都度に検証)
    patternElems.forEach( (elem) => {
      elem.addEventListener('input', () => {
        //要素の値が変更されたら検証を実行
        isPatternMismatch(elem);
      });
    });

    //指定された要素と値が一致するかどうかを検証する関数
    const isNotEqualTo = (elem) => {
      //検証対象のクラス名
      const className = 'equal-to';
      //対象の(比較対象の要素の id が記述されている)data-xxxx 属性(data-equal-to)
      const attributeName = 'data-' + className;
      //比較対象の要素の id
      const equalTo = elem.getAttribute(attributeName);
      //比較対象の要素
      const equalToElem = document.getElementById(equalTo);
      //エラーを表示する span 要素がすでに存在すれば取得
      const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className);
      //対象の要素の値が空でなければ値が同じかを検証
      if(elem.value.trim() !=='' && equalToElem.value.trim() !=='') {
        if(equalToElem.value !== elem.value) {
          if(!errorSpan) {
            addError(elem, className, '入力された値が一致しません');
          }
          return true;
        }else{
          if(errorSpan) {
            elem.parentNode.removeChild(errorSpan);
          }
          return false;
        }
      }
    }
    //equal-to クラスを指定された要素に input イベントを設定(値が変更される都度に検証)
    equalToElems.forEach( (elem) => {
      elem.addEventListener('input', () => {
        isNotEqualTo(elem);
      });
      //値を比較する要素(data-equal-to 属性に指定されている id を持つ要素)を取得
      const compareTarget = document.getElementById(elem.getAttribute('data-equal-to'));
      if(compareTarget) {
        //値を比較する要素の値が変更された場合も、値が同じかどうかを検証
        compareTarget.addEventListener('input', () => {
          isNotEqualTo(elem);
        });
      }
    });

    //サロゲートペアを考慮した文字数を返す関数
    const getValueLength = (value) => {
      return (value.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]/g) || []).length;
    }

    //指定された最小文字数を満たしているかを検証する関数(満たしていない場合は true を返す)
    const isTooShort = (elem) => {
      //対象のクラス名
      const className = 'minlength';
      //対象の data-xxxx 属性の名前
      const attributeName = 'data-' + className;
      //data-minlength 属性から最小文字数を取得
      const minlength = elem.getAttribute(attributeName);
      //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る)
      const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className);
      //値が空でなければ
      if(elem.value !=='') {
        //サロゲートペアを考慮した文字数を取得
        const valueLength = getValueLength(elem.value);
        //値がdata-minlength属性で指定された最小文字数より小さければエラーを表示してtrueを返す
        if(valueLength < minlength) {
          if(!errorSpan) {
            addError(elem, className, minlength + '文字以上で入力ください');
          }
          return true;
        //最小文字数より大きければエラーがあれば削除して false を返す
        }else{
          if(errorSpan) {
            elem.parentNode.removeChild(errorSpan);
          }
          return false;
        }
      //値が空でエラーを表示する要素が存在すれば削除
      }else if(elem.value ==='' && errorSpan) {
        elem.parentNode.removeChild(errorSpan);
      }
    }

    //minlength クラスを指定された要素に input イベントを設定(値が変更される都度に検証)
    minlengthElems.forEach( (elem) => {
      elem.addEventListener('input', () => {
        isTooShort(elem);
      });
    });

    //指定された最大文字数を満たしているかを検証する関数(満たしていない場合は true を返す)
    const isTooLong = (elem) => {
      //対象のクラス名
      const className = 'maxlength';
      //対象の data-xxxx 属性の名前
      const attributeName = 'data-' + className;
      //data-maxlength 属性から最大文字数を取得
      const maxlength = elem.getAttribute(attributeName);
      //エラーを表示する span 要素がすでに存在すれば取得(存在しなければ null が返る)
      const errorSpan = elem.parentElement.querySelector('.' + errorClassName + '.' + className);
      if(elem.value !=='') {
        //サロゲートペアを考慮した文字数を取得
        const valueLength = getValueLength(elem.value);
        //値がdata-maxlengthで指定された最大文字数より大きい場合はエラーを表示してtrueを返す
        if(valueLength > maxlength) {
          if(!errorSpan) {
            addError(elem, className, maxlength + '文字以内で入力ください');
          }
          return true;
        }else{
          if(errorSpan) {
            elem.parentNode.removeChild(errorSpan);
          }
          return false;
        }
      }else if(elem.value ==='' && errorSpan) {
        elem.parentNode.removeChild(errorSpan);
      }
    }

    //maxlength クラスを指定された要素に input イベントを設定
    maxlengthElems.forEach( (elem) => {
      elem.addEventListener('input', () => {
        isTooLong(elem);
      });
    });

    //data-maxlength属性を指定した要素でshowCountクラスが指定されていれば入力文字数を表示
    showCountElems.forEach( (elem) => {
      //data-maxlength 属性の値を取得
      const dataMaxlength = elem.getAttribute('data-maxlength');
      //data-maxlength 属性の値が存在し数値であれば
      if(dataMaxlength && !isNaN(dataMaxlength)) {
        //入力文字数を表示する p 要素を生成
        const countElem = document.createElement('p');
        //生成した p 要素にクラス countSpanWrapper を設定
        countElem.classList.add('countSpanWrapper');
        //p要素のコンテンツを作成(.countSpanを指定したspan要素にカウントを出力。初期値は0)
        countElem.innerHTML = '<span class="countSpan">0</span>/' + parseInt(dataMaxlength);
        //入力文字数を表示する p 要素を追加
        elem.parentNode.appendChild(countElem);
      }
      //input イベントを設定
      elem.addEventListener('input', (e) => {
        //上記で作成したカウントを出力する span 要素を取得
        const countSpan = elem.parentElement.querySelector('.countSpan');
        //カウントを出力する span 要素が存在すれば
        if(countSpan) {
          //入力されている文字数(e.currentTarget は elem. のこと)
          //サロゲートペアを考慮した文字数を取得
          const count = getValueLength(e.currentTarget.value);
          //span 要素に文字数を出力
          countSpan.textContent = count;
          //文字数が dataMaxlength(data-maxlength 属性の値)より大きい場合は文字を赤色に
          if(count > dataMaxlength) {
            countSpan.style.setProperty('color', 'red');
            //span 要素に overMaxCount クラス(スタイル設定用)を追加
            countSpan.classList.add('overMaxCount');
          }else{
            //dataMaxlength 未満の場合は文字を元に戻す
            countSpan.style.removeProperty('color');
            //span 要素から overMaxCount クラスを削除
            countSpan.classList.remove('overMaxCount');
          }
        }
      });
    });

    //送信時の処理
    validationForm.addEventListener('submit', (e) => {
      validateAfterFirstSubmit = false;
      //必須の検証
      requiredElems.forEach( (elem) => {
        if(isValueMissing(elem)) {
          e.preventDefault();
        }
      });
      //パターンの検証
      patternElems.forEach( (elem) => {
        if(isPatternMismatch(elem)) {
          e.preventDefault();
        }
      });
      //.minlength を指定した要素の検証
      minlengthElems.forEach( (elem) => {
        if(isTooShort(elem)) {
          e.preventDefault();
        }
      });
      //.maxlength を指定した要素の検証
      maxlengthElems.forEach( (elem) => {
        if(isTooLong(elem)) {
          e.preventDefault();
        }
      });
      //2つの値(メールアドレス)が一致するかどうかを検証
      equalToElems.forEach( (elem) => {
        if(isNotEqualTo(elem)) {
          e.preventDefault();
        }
      });

      //.error の要素を取得
      const errorElem = document.querySelector('.' + errorClassName);
      if(errorElem) {
        const errorElemOffsetTop = errorElem.offsetTop;
        //エラーの要素の位置へスクロール
        window.scrollTo({
          top: errorElemOffsetTop - 40,
          //スムーススクロール
          behavior: 'smooth'
        });
      }
    });
  }
});

自動返信

コンタクトフォームから送信されたメールに自動的にメールを返信する機能を追加する例です。

前述のサンプルの PHP と HTML の一部を変更します。JavaScript は同じです。

サンプルを別ページで開く

PHP

お問い合わせメールの送信が成功した場合に、送信者の情報をもとにヘッダー情報を生成し、送信された内容(件名やお問い合わせ内容)からメール本文を作成して返信します(以下6行目〜26行目)。

AUTO_REPLY_NAME(自動返信の返信先名前)や MAIL_TO(メールの宛先)は mailvars.php で定義してある値を使います。

自動返信メールの本文は POST メソッドで送信された値を格納した変数 $name や $body などを使って作成し、返信の送信先はユーザのアドレス $email を指定します。

以下では本文にお問い合わせを送信した時刻を取得して表示するようにしています。

また、40行目では、リダイレクト先の自身の URL パラメータに自動返信の送信結果($result2)を追加しています。

PHP 一部抜粋
//メール送信の結果判定($result が true の場合以下を実行)
if ( $result ) {

  //自動返信メール
  //ヘッダー情報
  $ar_header = "MIME-Version: 1.0\n";
  // AUTO_REPLY_NAME や MAIL_TO は mailvars.php で定義
  $ar_header .= "From: " . mb_encode_mimeheader( AUTO_REPLY_NAME ) . " <" . MAIL_TO . ">\n";
  $ar_header .= "Reply-To: " . mb_encode_mimeheader( AUTO_REPLY_NAME ) . " <" . MAIL_TO . ">\n";
  //件名
  $ar_subject = 'お問い合わせ自動返信メール';
  //本文
  $ar_body = $name." 様\n\n";
  $ar_body .= "この度は、お問い合わせ頂き誠にありがとうございます。" . "\n\n";
  $ar_body .= "下記の内容でお問い合わせを受け付けました。\n\n";
  $ar_body .= "お問い合わせ日時:" . date("Y年m月d日 D H時i分") . "\n";
  $ar_body .= "お名前:" . $name . "\n";
  $ar_body .= "メールアドレス:" . $email . "\n";
  $ar_body .= "お電話番号: " . $tel . "\n\n" ;
  $ar_body .="<お問い合わせ内容>" . "\n" . $body;

  //自動返信メールを送信(送信結果を変数 $result2 に代入)
  if ( ini_get( 'safe_mode' ) ) {
    $result2 = mb_send_mail( $email, $ar_subject, $ar_body , $ar_header  );
  } else {
    $result2 = mb_send_mail( $email, $ar_subject, $ar_body , $ar_header , '-f' . $returnMail );
  }

  $_POST = array(); //空の配列を代入し、すべてのPOST変数を消去
  //変数の値も初期化
  $name = '';
  $email = '';
  $email_check = '';
  $tel = '';
  $subject = '';
  $body = '';

  //再読み込みによる二重送信の防止
  //自動返信の送信結果($result2)をパラメータに追加
  $params = '?result='. $result .'&result2=' . $result2;
  //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
  if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
    $_SERVER['HTTPS'] = 'on';
  }
  $url = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://').$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];
  header('Location:' . $url . $params);
  exit;
}

HTML

HTML では自動返信の送信結果 $_GET['result2'] を filter_input() に真偽値の検証フィルタ(FILTER_VALIDATE_BOOLEAN)を指定して取得し、その値による表示を追加しています(4〜8行目)。

ユーザへ自動送信した宛先アドレスも $_GET を使えば「自動返信メールを xxxx@xxxx.com へお送りいたしました」のように表示できますが、URL のパラメータに表示されてしまうので、あえて表示しないようにしています。

その他の部分は前述のサンプルと同じです。

自動返信メールが送信できなかった場合のメッセージなどは必要に応じて書き換えます。

<?php if (filter_input(INPUT_GET, 'result') ) : // 送信が成功した場合?>
<h4>送信完了!</h4>
<p>送信完了いたしました。</p>
<?php  if ( filter_input(INPUT_GET, 'result2', FILTER_VALIDATE_BOOLEAN) ) : ?>
  <p class="success">確認の自動返信メールをお送りいたしました。</p>
<?php elseif ( !filter_input(INPUT_GET, 'result2', FILTER_VALIDATE_BOOLEAN) ): ?>
  <p class="fail">確認の自動返信メールが送信できませんでした。</p>
<?php endif; ?>
<hr>
<?php elseif (isset($result) && !$result ): ?>
  <h4>送信失敗</h4>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>メール:<a href="mailto:info@example.com">Contact</a></p>
  <hr>
<?php endif; ?>

<?php
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';

// 値が null であれば、空文字列に変換する関数
function nullToString($val) {
  if($val === null) return '';
  return $val;
}

//POSTされたデータの前後にあるホワイトスペースを削除してを変数に格納
$name = trim( nullToString(filter_input(INPUT_POST, 'name')) );
$email = trim( nullToString(filter_input(INPUT_POST, 'email')) );
$email_check = trim( nullToString(filter_input(INPUT_POST, 'email_check')) );
$tel = trim( nullToString(filter_input(INPUT_POST, 'tel')) );
$subject = trim( nullToString(filter_input(INPUT_POST, 'subject')) );
$body = trim( nullToString(filter_input(INPUT_POST, 'body')) );

if (isset($_POST['submitted'])) {

  //POSTされたデータをチェック
  $_POST = checkInput( $_POST );

  //エラーメッセージを保存する配列の初期化
  $error = array();

  //値の検証
  if ( $name == '' ) {
    $error['name'] = '*お名前は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,30}\z/u', $name ) == 0 ) {
    $error['name'] = '*お名前は30文字以内でお願いします。';
  }
  if ( $email == '' ) {
    $error['email'] = '*メールアドレスは必須です。';
  } else { //メールアドレスを正規表現でチェック
    $pattern = '/\A([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}\z/uiD';
    if ( !preg_match( $pattern, $email ) ) {
      $error['email'] = '*メールアドレスの形式が正しくありません。';
    }
  }
  if ( $email_check == '' ) {
    $error['email_check'] = '*確認用メールアドレスは必須です。';
  } else { //メールアドレスを正規表現でチェック
    if ( $email_check !== $email ) {
      $error['email_check'] = '*メールアドレスが一致しません。';
    }
  }
  if ( $tel != '' && preg_match( '/\A\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}\z/u', $tel ) == 0 ) {
    $error['tel'] = '*電話番号の形式が正しくありません。';
  }
  if ( $subject == '' ) {
    $error['subject'] = '*件名は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,100}\z/u', $subject ) == 0 ) {
    $error['subject'] = '*件名は100文字以内でお願いします。';
  }
  if ( $body == '' ) {
    $error['body'] = '*内容は必須項目です。';
    //制御文字(タブ、復帰、改行を除く)でないことと文字数をチェック
  } else if ( preg_match( '/\A[\r\n\t[:^cntrl:]]{1,1050}\z/u', $body ) == 0 ) {
    $error['body'] = '*内容は1000文字以内でお願いします。';
  }

  //エラーがなく且つ POST でのリクエストの場合
  if (empty($error) && $_SERVER['REQUEST_METHOD']==='POST') {
    //メールアドレス等を記述したファイルの読み込み
    require '../libs/mailvars.php';

    //メール本文の組み立て
    $mail_body = 'コンタクトページからのお問い合わせ' . "\n\n";
    $mail_body .=  date("Y年m月d日 D H時i分") . "\n\n";
    $mail_body .=  "お名前: " .h($name) . "\n";
    $mail_body .=  "Email: " . h($email) . "\n"  ;
    $mail_body .=  "お電話番号: " . h($tel) . "\n\n" ;
    $mail_body .=  "<お問い合わせ内容>" . "\n" . h($body);

    //--------sendmail------------

    //メールの宛先(名前<メールアドレス> の形式)。値は mailvars.php に記載
    $mailTo = mb_encode_mimeheader(MAIL_TO_NAME) ."<" . MAIL_TO. ">";

    //Return-Pathに指定するメールアドレス
    $returnMail = MAIL_RETURN_PATH; //
    //mbstringの日本語設定
    mb_language( 'ja' );
    mb_internal_encoding( 'UTF-8' );

    // 送信者情報(From ヘッダー)の設定
    $header = "From: " . mb_encode_mimeheader($name) ."<" . $email. ">\n";
    $header .= "Cc: " . mb_encode_mimeheader(MAIL_CC_NAME) ."<" . MAIL_CC.">\n";
    $header .= "Bcc: <" . MAIL_BCC.">";

    //メールの送信(送信結果を変数 $result に代入)
    if ( ini_get( 'safe_mode' ) ) {
      //セーフモードがOnの場合は第5引数が使えない
      $result = mb_send_mail( $mailTo, $subject, $mail_body, $header );
    } else {
      $result = mb_send_mail( $mailTo, $subject, $mail_body, $header, '-f' . $returnMail );
    }

    //メール送信の結果判定
    if ( $result ) {

      //自動返信メール
      //ヘッダー情報
      $ar_header = "MIME-Version: 1.0\n";
      $ar_header .= "From: " . mb_encode_mimeheader( AUTO_REPLY_NAME ) . " <" . MAIL_TO . ">\n";
      $ar_header .= "Reply-To: " . mb_encode_mimeheader( AUTO_REPLY_NAME ) . " <" . MAIL_TO . ">\n";
      //件名
      $ar_subject = 'お問い合わせ自動返信メール';
      //本文
      $ar_body = $name." 様\n\n";
      $ar_body .= "この度は、お問い合わせ頂き誠にありがとうございます。" . "\n\n";
      $ar_body .= "下記の内容でお問い合わせを受け付けました。\n\n";
      $ar_body .= "お問い合わせ日時:" . date("Y年m月d日 D H時i分") . "\n";
      $ar_body .= "お名前:" . $name . "\n";
      $ar_body .= "メールアドレス:" . $email . "\n";
      $ar_body .= "お電話番号: " . $tel . "\n\n" ;
      $ar_body .="<お問い合わせ内容>" . "\n" . $body;

      //自動返信メールを送信(送信結果を変数 $result2 に代入)
      if ( ini_get( 'safe_mode' ) ) {
        $result2 = mb_send_mail( $email, $ar_subject, $ar_body , $ar_header  );
      } else {
        $result2 = mb_send_mail( $email, $ar_subject, $ar_body , $ar_header , '-f' . $returnMail );
      }

      $_POST = array(); //空の配列を代入し、すべてのPOST変数を消去
      //変数の値も初期化
      $name = '';
      $email = '';
      $email_check = '';
      $tel = '';
      $subject = '';
      $body = '';

      //再読み込みによる二重送信の防止
      $params = '?result='. $result .'&result2=' . $result2;
      //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
      if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
        $_SERVER['HTTPS'] = 'on';
      }
      $url = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://').$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];
      header('Location:' . $url . $params);
      exit;
    }
  }
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>コンタクトフォーム</title>
<link href="../bootstrap.min.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="container">
  <h2 class="">お問い合わせフォーム</h2>
  <?php if (filter_input(INPUT_GET, 'result') ) : // 送信が成功した場合?>
  <h4>送信完了!</h4>
  <p>送信完了いたしました。</p>
  <?php  if ( filter_input(INPUT_GET, 'result2', FILTER_VALIDATE_BOOLEAN) ) : ?>
    <p class="success">確認の自動返信メールをお送りいたしました。</p>
  <?php elseif ( !filter_input(INPUT_GET, 'result2', FILTER_VALIDATE_BOOLEAN) ): ?>
    <p class="fail">確認の自動返信メールが送信できませんでした。</p>
  <?php endif; ?>
  <hr>
  <?php elseif (isset($result) && !$result ): ?>
    <h4>送信失敗</h4>
    <p>申し訳ございませんが、送信に失敗しました。</p>
    <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
    <p>メール:<a href="mailto:info@example.com">Contact</a></p>
    <hr>
  <?php endif; ?>
  <p>以下のフォームからお問い合わせください。</p>
  <form id="form" class="validationForm" method="post" novalidate>
    <div class="form-group">
      <label for="name">お名前(必須)
        <span class="error-php"><?php if ( isset( $error['name'] ) ) echo h( $error['name'] ); ?></span>
      </label>
      <input type="text" class="required maxlength form-control" data-maxlength="30" id="name" name="name" placeholder="氏名" data-error-required="お名前は必須です。" value="<?php echo h($name); ?>">
    </div>
    <div class="form-group">
      <label for="email">Email(必須)
        <span class="error-php"><?php if ( isset( $error['email'] ) ) echo h( $error['email'] ); ?></span>
      </label>
      <input type="email" class="required pattern form-control" data-pattern="email" id="email" name="email" placeholder="Email アドレス" data-error-required="Email アドレスは必須です。"  data-error-pattern="Email の形式が正しくないようですのでご確認ください" value="<?php echo h($email); ?>">
    </div>
    <div class="form-group">
      <label for="email_check">Email(確認用 必須)
        <span class="error-php"><?php if ( isset( $error['email_check'] ) ) echo h( $error['email_check'] ); ?></span>
      </label>
      <input type="email"  class="form-control equal-to required" data-equal-to="email" data-error-equal-to="メールアドレスが異なります" id="email_check" name="email_check" placeholder="Email アドレス(確認用 必須)" value="<?php echo h($email_check); ?>">
    </div>
    <div class="form-group">
      <label for="tel">お電話番号(半角英数字)
        <span class="error-php"><?php if ( isset( $error['tel'] ) ) echo h( $error['tel'] ); ?></span>
      </label>
      <input type="tel" class="pattern form-control" data-pattern="tel" id="tel" name="tel" placeholder="お電話番号" data-error-pattern="電話番号の形式が正しくないようですのでご確認ください"  value="<?php echo h($tel); ?>">
    </div>
    <div class="form-group">
      <label for="subject">件名(必須)
        <span class="error-php"><?php if ( isset( $error['subject'] ) ) echo h( $error['subject'] ); ?></span>
      </label>
      <input type="text" class="required maxlength form-control" data-maxlength="100" id="subject" name="subject" placeholder="件名" value="<?php echo h($subject); ?>">
    </div>
    <div class="form-group">
      <label for="body">お問い合わせ内容(必須)
        <span class="error-php"><?php if ( isset( $error['body'] ) ) echo h( $error['body'] ); ?></span>
      </label>
      <textarea class="required maxlength showCount form-control" data-maxlength="1000" id="body" name="body" placeholder="お問い合わせ内容(1000文字まで)をお書きください" rows="3"><?php echo h($body); ?></textarea>
    </div>
    <button name="submitted" type="submit" class="btn btn-primary">送信</button>
  </form>
</div>
<!--  検証用の JavaScriptの読み込み(または script タグに検証用スクリプトを記述) -->
<script src="formValidation.js"></script>
</body>
</html>

PHPMailer を使う

添付ファイルなどを送信する必要がある場合は mb_send_mail() の代わりに PHPMailer を使うと簡単かも知れません。但し、ライブラリを更新するなどの必要があるので実際に使用する場合は検討が必要です。

関連ページPHPMailer の使い方

以下は mb_send_mail() の代わりに PHPMailer を使ってメールを送信する場合の PHP の例です。

4〜6行目は PHPMailer を使うのに必要な記述です。use は if ブロックなどブロック内のスコープではエラーになるので先頭に記述しています。

メールの送信処理以外は前述の例と同じですが、PHPMailer を使うにはメールアカウントのパスワードなどを設定する必要があるのでそれらの情報は別途 phpmailvars.php というファイルに記述して読み込んでいます(94行目)。

メールの送信処理では PHPMailer のインスタンスを生成し(101行目)、そのプロパティやメソッドを使って送信処理をします。自動返信の処理では新たにインスタンスを生成する必要があります(150行目)。

109行目や153行目のコメントアウトを外すと、デバグの結果が全て出力されます(メインテナンス用)。また、143行目や191行目のコメントアウトを外すと送信ができなかった際に PHPMailer によるエラーメッセージが表示されます。

<?php
//PHPMailer 名前空間の使用(ファイル内の一番外側のスコープで行う)
//ブロック内のスコープではインポートできない
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

//PHPMailer の読み込み  Load Composer's autoloader
require '../php_mailer/vendor/autoload.php';


//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';

//お問い合わせ日時を日本時間に
date_default_timezone_set('Asia/Tokyo');

// 値が null であれば、空文字列に変換する関数
function nullToString($val) {
  if($val === null) return '';
  return $val;
}
//POSTされたデータの前後にあるホワイトスペースを削除してを変数に格納
$name = trim( nullToString(filter_input(INPUT_POST, 'name')) );
$email = trim( nullToString(filter_input(INPUT_POST, 'email')) );
$email_check = trim( nullToString(filter_input(INPUT_POST, 'email_check')) );
$tel = trim( nullToString(filter_input(INPUT_POST, 'tel')) );
$subject = trim( nullToString(filter_input(INPUT_POST, 'subject')) );
$body = trim( nullToString(filter_input(INPUT_POST, 'body')) );

if (isset($_POST['submitted'])) {

  //POSTされたデータをチェック
  $_POST = checkInput( $_POST );

  //エラーメッセージを保存する配列の初期化
  $error = array();

  //値の検証
  if ( $name == '' ) {
    $error['name'] = '*お名前は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,30}\z/u', $name ) == 0 ) {
    $error['name'] = '*お名前は30文字以内でお願いします。';
  }
  if ( $email == '' ) {
    $error['email'] = '*メールアドレスは必須です。';
  } else { //メールアドレスを正規表現でチェック
    $pattern = '/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/uiD';
    if ( !preg_match( $pattern, $email ) ) {
      $error['email'] = '*メールアドレスの形式が正しくありません。';
    }
  }
  if ( $email_check == '' ) {
    $error['email_check'] = '*確認用メールアドレスは必須です。';
  } else { //メールアドレスを正規表現でチェック
    if ( $email_check !== $email ) {
      $error['email_check'] = '*メールアドレスが一致しません。';
    }
  }
  if ( preg_match( '/\A[[:^cntrl:]]{0,30}\z/u', $tel ) == 0 ) {
    $error['tel'] = '*電話番号は30文字以内でお願いします。';
  }
  if ( $tel != '' && preg_match( '/\A\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}\z/u', $tel ) == 0 ) {
    $error['tel_format'] = '*電話番号の形式が正しくありません。';
  }
  if ( $subject == '' ) {
    $error['subject'] = '*件名は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,100}\z/u', $subject ) == 0 ) {
    $error['subject'] = '*件名は100文字以内でお願いします。';
  }
  if ( $body == '' ) {
    $error['body'] = '*内容は必須項目です。';
    //制御文字(タブ、復帰、改行を除く)でないことと文字数をチェック
  } else if ( preg_match( '/\A[\r\n\t[:^cntrl:]]{1,1050}\z/u', $body ) == 0 ) {
    $error['body'] = '*内容は1000文字以内でお願いします。';
  }

  //エラーがなく且つ POST でのリクエストの場合
  if (empty($error) && $_SERVER['REQUEST_METHOD']==='POST') {

    //メール本文の組み立て
    $mail_body = 'コンタクトページからのお問い合わせ' . "\n\n";
    $mail_body .=  date("Y年m月d日 D H時i分") . "\n\n";
    $mail_body .=  "お名前: " .h($name) . "\n";
    $mail_body .=  "Email: " . h($email) . "\n"  ;
    $mail_body .=  "お電話番号: " . h($tel) . "\n\n" ;
    $mail_body .=  "<お問い合わせ内容>" . "\n" . h($body);

    //--------PHPMailer------------

    //メールアカウント情報(パスワード等)の読み込み PHPMailer用
    require '../libs/phpmailvars.php';

    //mbstring の日本語設定
    mb_language( "japanese" );
    mb_internal_encoding( "UTF-8" );

    //PHPMailer のインスタンスを生成
    $mail = new PHPMailer( true );

    //送信結果の真偽値の初期化
    $result = false;
    $result2 = false;

    try {
      //サーバ設定
      //$mail->SMTPDebug = SMTP::DEBUG_SERVER; // デバグの出力を有効に
      $mail->isSMTP(); // SMTP を使用
      $mail->Host = MAIL_HOST; // SMTP サーバーを指定(phpmailvars.phpで定義)
      $mail->SMTPAuth = true; // SMTP authentication を有効に
      $mail->Username = MAIL_USER; // SMTP ユーザ名(phpmailvars.phpで定義)
      $mail->Password = MAIL_PASSWORD; // SMTP パスワード(phpmailvars.phpで定義)
      $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // TLS を有効に
      $mail->Port = 587; // TCP ポートを指定(TLS の場合のポート番号)

      //日本語用
      $mail->CharSet = "iso-2022-jp";
      $mail->Encoding = "7bit";

      //差出人アドレス, 差出人名
      $mail->setFrom( $email, mb_encode_mimeheader( $name ) );
      //送信先アドレス・宛先名(phpmailvars.phpで定義)
      $mail->AddAddress(SEND_TO, mb_encode_mimeheader(SEND_TO_NAME));
      //Bcc アドレス(phpmailvars.phpで定義)
      $mail->AddBcc( BCC );

      $mail->isHTML( false ); // Set email format to plain text
      $mail->Subject = mb_encode_mimeheader( $subject ); //件名
      $mail->WordWrap = 70; //70 文字で改行(好みで)

      $mail->Body = mb_convert_encoding( $mail_body, "JIS", "UTF-8" );
      //テキスト表示の本文
      //$mail->AltBody = mb_convert_encoding($mail_body,"JIS","UTF-8");

      //メール送信の結果(真偽値)を $result に代入
      $result = $mail->send();

    } catch ( Exception $e ) {
      $result = false;
      //PHPMailer のエラーメッセージ(例外が発生した場合)
      //echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
    }

    //メール送信の結果判定
    if ( $result ) {

      //自動返信メール
      $autoresponder = new PHPMailer( true );
      try {
        //サーバ設定
        //$autoresponder->SMTPDebug = SMTP::DEBUG_SERVER; // デバグの出力を有効に
        $autoresponder->isSMTP(); // SMTP を使用
        $autoresponder->Host = MAIL_HOST; // SMTP サーバーを指定
        $autoresponder->SMTPAuth = true; // SMTP authentication を有効に
        $autoresponder->Username = MAIL_USER; // SMTP ユーザ名
        $autoresponder->Password = MAIL_PASSWORD; // SMTP パスワード
        $autoresponder->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; //TLS を有効に
        $autoresponder->Port = 587; // TCP ポートを指定

        //日本語用
        $autoresponder->CharSet = "iso-2022-jp";
        $autoresponder->Encoding = "7bit";

        //差出人アドレス, 差出人名
        $autoresponder->setFrom( AR_SEND_FROM, mb_encode_mimeheader( AR_SEND_FROM_NAME ) );
        //送信先・宛先
        $autoresponder->AddAddress( $email, mb_encode_mimeheader( $name ) );
        $autoresponder->isHTML( false ); // メールのフォーマットをテキストに
        $autoresponder->Subject = mb_encode_mimeheader( "自動返信メール" ); //件名
        //返信用アドレス(差出人以外に別途指定する場合)
        $autoresponder->addReplyTo( MAIL_USER, mb_encode_mimeheader("お問い合わせ"));
        $autoresponder->WordWrap = 70; //70 文字で改行(好みで)
        $ar_body = $name." 様\n\n";
        $ar_body .= "この度は、お問い合わせ頂き誠にありがとうございます。" . "\n\n";
        $ar_body .= "下記の内容でお問い合わせを受け付けました。\n\n";
        $ar_body .= "お問い合わせ日時:" . date("Y-m-d H:i") . "\n";
        $ar_body .= "お名前:" . $name . "\n";
        $ar_body .= "メールアドレス:" . $email . "\n";
        $ar_body .= "お電話番号: " . $tel . "\n\n" ;
        $ar_body .="<お問い合わせ内容>" . "\n" . $body;
        $autoresponder->Body = mb_convert_encoding( $ar_body, "JIS", "UTF-8" );

        //自動送信メールの送信結果(真偽値)を result2 に代入
        $result2 = $autoresponder->send();

      } catch ( Exception $e ) {
        $result2 = false; //例外が発生した場合は結果を false に
        //PHPMailer のエラーメッセージ(例外が発生した場合)
        //echo "Auto Response Message could not be sent. Mailer Error: {$autoresponder->ErrorInfo}";
      }

      //空の配列を代入し、すべてのPOST変数を消去
      $_POST = array();
      //変数の値も初期化
      $name = '';
      $email = '';
      $email_check = '';
      $tel = '';
      $subject = '';
      $body = '';

      $params = '?result='. $result .'&result2=' . $result2;
      //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
      if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
        $_SERVER['HTTPS'] = 'on';
      }
      $url = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://').$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];
      header('Location:' . $url . $params);
      exit;
    }
  }
}
?>

以下は PHPMailer を使用するためのアカウント情報などを記述したファイルの例です。パスワードなどを記述してあるので、パブリックからアクセスできない場所に保存するか .htaccess などで外部からアクセスできないようにします。

phpmailvars.php
<?php
//SMTP サーバー(サーバーの場合:localhost でも可)
define('MAIL_HOST', 'mail.xxxxxx.com');

//PHPMailer を使って送信するための E-mail アカウント
define('MAIL_USER', 'xxxxx@xxxxxxx.com');

//パスワード
define('MAIL_PASSWORD', 'xxxxxxxxxx');

//送信先
define('SEND_TO', 'xxxx@xxxxxx.com');

//送信先の名前
define('SEND_TO_NAME', '送信先名前');

//Bcc アドレス
define('BCC', 'xxxx@xxxxxx.com');

//自動返信をする場合の送信元アドレス
define('AR_SEND_FROM', 'xxxx@xxxxxxxx.com');

//自動返信をする場合の送信元名前
define('AR_SEND_FROM_NAME', '自動返信送信元名前');
<?php
//PHPMailer 名前空間の使用(ファイル内の一番外側のスコープで行う)
//ブロック内のスコープではインポートできない
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

//PHPMailer の読み込み  Load Composer's autoloader
require '../php_mailer/vendor/autoload.php';

//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';

//お問い合わせ日時を日本時間に
date_default_timezone_set('Asia/Tokyo');

// 値が null であれば、空文字列に変換する関数
function nullToString($val) {
  if($val === null) return '';
  return $val;
}
//POSTされたデータの前後にあるホワイトスペースを削除してを変数に格納
$name = trim( nullToString(filter_input(INPUT_POST, 'name')) );
$email = trim( nullToString(filter_input(INPUT_POST, 'email')) );
$email_check = trim( nullToString(filter_input(INPUT_POST, 'email_check')) );
$tel = trim( nullToString(filter_input(INPUT_POST, 'tel')) );
$subject = trim( nullToString(filter_input(INPUT_POST, 'subject')) );
$body = trim( nullToString(filter_input(INPUT_POST, 'body')) );

if (isset($_POST['submitted'])) {

  //POSTされたデータをチェック
  $_POST = checkInput( $_POST );

  //エラーメッセージを保存する配列の初期化
  $error = array();

  //値の検証
  if ( $name == '' ) {
    $error['name'] = '*お名前は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,30}\z/u', $name ) == 0 ) {
    $error['name'] = '*お名前は30文字以内でお願いします。';
  }
  if ( $email == '' ) {
    $error['email'] = '*メールアドレスは必須です。';
  } else { //メールアドレスを正規表現でチェック
    $pattern = '/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/uiD';
    if ( !preg_match( $pattern, $email ) ) {
      $error['email'] = '*メールアドレスの形式が正しくありません。';
    }
  }
  if ( $email_check == '' ) {
    $error['email_check'] = '*確認用メールアドレスは必須です。';
  } else { //メールアドレスを正規表現でチェック
    if ( $email_check !== $email ) {
      $error['email_check'] = '*メールアドレスが一致しません。';
    }
  }
  if ( preg_match( '/\A[[:^cntrl:]]{0,30}\z/u', $tel ) == 0 ) {
    $error['tel'] = '*電話番号は30文字以内でお願いします。';
  }
  if ( $tel != '' && preg_match( '/\A\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}\z/u', $tel ) == 0 ) {
    $error['tel_format'] = '*電話番号の形式が正しくありません。';
  }
  if ( $subject == '' ) {
    $error['subject'] = '*件名は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,100}\z/u', $subject ) == 0 ) {
    $error['subject'] = '*件名は100文字以内でお願いします。';
  }
  if ( $body == '' ) {
    $error['body'] = '*内容は必須項目です。';
    //制御文字(タブ、復帰、改行を除く)でないことと文字数をチェック
  } else if ( preg_match( '/\A[\r\n\t[:^cntrl:]]{1,1050}\z/u', $body ) == 0 ) {
    $error['body'] = '*内容は1000文字以内でお願いします。';
  }

  //エラーがなく且つ POST でのリクエストの場合
  if (empty($error) && $_SERVER['REQUEST_METHOD']==='POST') {

    //メール本文の組み立て
    $mail_body = 'コンタクトページからのお問い合わせ' . "\n\n";
    $mail_body .=  date("Y年m月d日 D H時i分") . "\n\n";
    $mail_body .=  "お名前: " .h($name) . "\n";
    $mail_body .=  "Email: " . h($email) . "\n"  ;
    $mail_body .=  "お電話番号: " . h($tel) . "\n\n" ;
    $mail_body .=  "<お問い合わせ内容>" . "\n" . h($body);

    //--------PHPMailer------------

    //メールアカウント情報(パスワード等)の読み込み PHPMailer用
    require '../libs/phpmailvars.php';

    //mbstring の日本語設定
    mb_language( "japanese" );
    mb_internal_encoding( "UTF-8" );

    //PHPMailer のインスタンスを生成 Instantiation and passing `true` enables exceptions
    $mail = new PHPMailer( true );

    //送信結果の真偽値の初期化
    $result = false;
    $result2 = false;

    try {
      //サーバ設定
      //$mail->SMTPDebug = SMTP::DEBUG_SERVER; // デバグの出力を有効に
      $mail->isSMTP(); // SMTP を使用
      $mail->Host = MAIL_HOST; // SMTP サーバーを指定(phpmailvars.phpで定義)
      $mail->SMTPAuth = true; // SMTP authentication を有効に
      $mail->Username = MAIL_USER; // SMTP ユーザ名(phpmailvars.phpで定義)
      $mail->Password = MAIL_PASSWORD; // SMTP パスワード(phpmailvars.phpで定義)
      $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // TLS を有効に
      $mail->Port = 587; // TCP ポートを指定(TLS の場合のポート番号)

      //日本語用
      $mail->CharSet = "iso-2022-jp";
      $mail->Encoding = "7bit";

      //Recipients
      $mail->setFrom( $email, mb_encode_mimeheader( $name ) ); //差出人アドレス, 差出人名
      $mail->AddAddress(SEND_TO, mb_encode_mimeheader(SEND_TO_NAME)); //送信先アドレス・宛先名(phpmailvars.phpで定義)
      $mail->AddBcc( BCC ); //Bcc アドレス(phpmailvars.phpで定義)

      $mail->isHTML( false ); // Set email format to plain text
      $mail->Subject = mb_encode_mimeheader( $subject ); //件名
      $mail->WordWrap = 70; //70 文字で改行(好みで)

      $mail->Body = mb_convert_encoding( $mail_body, "JIS", "UTF-8" );
      //$mail->AltBody = mb_convert_encoding($mail_body,"JIS","UTF-8"); //テキスト表示の本文

      //メール送信の結果(真偽値)を $result に代入
      $result = $mail->send();

    } catch ( Exception $e ) {
      $result = false;
      //PHPMailer のエラーメッセージ
      //echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
    }

    //メール送信の結果判定
    if ( $result ) {

      //自動返信メール
      $autoresponder = new PHPMailer( true );
      try {
        //サーバ設定
        //$autoresponder->SMTPDebug = SMTP::DEBUG_SERVER; // デバグの出力を有効に
        $autoresponder->isSMTP(); // SMTP を使用
        $autoresponder->Host = MAIL_HOST; // SMTP サーバーを指定(phpmailvars.phpで定義)
        $autoresponder->SMTPAuth = true; // SMTP authentication を有効に
        $autoresponder->Username = MAIL_USER; // SMTP ユーザ名(phpmailvars.phpで定義)
        $autoresponder->Password = MAIL_PASSWORD; // SMTP パスワード(phpmailvars.phpで定義)
        $autoresponder->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; //TLS を有効に
        $autoresponder->Port = 587; // TCP ポートを指定

        //日本語用
        $autoresponder->CharSet = "iso-2022-jp";
        $autoresponder->Encoding = "7bit";

        //Recipients
        $autoresponder->setFrom( AR_SEND_FROM, mb_encode_mimeheader( AR_SEND_FROM_NAME ) ); //差出人アドレス, 差出人名
        $autoresponder->AddAddress( $email, mb_encode_mimeheader( $name ) ); //送信先・宛先(phpmailvars.phpで定義)

        $autoresponder->isHTML( false ); // Set email format to plain text
        $autoresponder->Subject = mb_encode_mimeheader( "自動返信メール" ); //件名
        //返信用アドレス(差出人以外に別途指定する場合)
        $autoresponder->addReplyTo( MAIL_USER, mb_encode_mimeheader("お問い合わせ"));
        $autoresponder->WordWrap = 70; //70 文字で改行(好みで)
        $ar_body = $name." 様\n\n";
        $ar_body .= "この度は、お問い合わせ頂き誠にありがとうございます。" . "\n\n";
        $ar_body .= "下記の内容でお問い合わせを受け付けました。\n\n";
        $ar_body .= "お問い合わせ日時:" . date("Y-m-d H:i") . "\n";
        $ar_body .= "お名前:" . $name . "\n";
        $ar_body .= "メールアドレス:" . $email . "\n";
        $ar_body .= "お電話番号: " . $tel . "\n\n" ;
        $ar_body .="<お問い合わせ内容>" . "\n" . $body;
        $autoresponder->Body = mb_convert_encoding( $ar_body, "JIS", "UTF-8" );

        //自動送信メールの送信結果(真偽値)を result2 に代入
        $result2 = $autoresponder->send();

      } catch ( Exception $e ) {
        //PHPMailer のエラーメッセージ
        //echo "Auto Response Message could not be sent. Mailer Error: {$autoresponder->ErrorInfo}";
      }

      //空の配列を代入し、すべてのPOST変数を消去
      $_POST = array();
      //変数の値も初期化
      $name = '';
      $email = '';
      $email_check = '';
      $tel = '';
      $subject = '';
      $body = '';

      $params = '?result='. $result .'&result2=' . $result2;
      //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
      if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
        $_SERVER['HTTPS'] = 'on';
      }
      $url = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://').$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];
      header('Location:' . $url . $params);
      exit;
    }
  }
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>コンタクトフォーム</title>
<link href="../bootstrap.min.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="container">
  <h2 class="">お問い合わせフォーム</h2>
  <?php if (filter_input(INPUT_GET, 'result') ) : // 送信が成功した場合?>
  <h4>送信完了!</h4>
  <p>送信完了いたしました。</p>
  <?php  if ( filter_input(INPUT_GET, 'result2', FILTER_VALIDATE_BOOLEAN) ) : ?>
    <p class="success">確認の自動返信メールをお送りいたしました。</p>
  <?php elseif ( !filter_input(INPUT_GET, 'result2', FILTER_VALIDATE_BOOLEAN) ): ?>
    <p class="fail">確認の自動返信メールが送信できませんでした。</p>
  <?php endif; ?>
  <hr>
  <?php elseif (isset($result) && !$result ): ?>
  <h4>送信失敗</h4>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>メール:<a href="mailto:info@example.com">Contact</a></p>
  <hr>
  <?php endif; ?>
  <p>以下のフォームからお問い合わせください。</p>
  <form id="form" class="validationForm" method="post" novalidate>
    <div class="form-group">
      <label for="name">お名前(必須)
        <span class="error-php"><?php if ( isset( $error['name'] ) ) echo h( $error['name'] ); ?></span>
      </label>
      <input type="text" class="required maxlength form-control" data-maxlength="30" id="name" name="name" placeholder="氏名" data-error-required="お名前は必須です。" value="<?php echo h($name); ?>">
    </div>
    <div class="form-group">
      <label for="email">Email(必須)
        <span class="error-php"><?php if ( isset( $error['email'] ) ) echo h( $error['email'] ); ?></span>
      </label>
      <input type="email" class="required pattern form-control" data-pattern="email" id="email" name="email" placeholder="Email アドレス" data-error-required="Email アドレスは必須です。"  data-error-pattern="Email の形式が正しくないようですのでご確認ください" value="<?php echo h($email); ?>">
    </div>
    <div class="form-group">
      <label for="email_check">Email(確認用 必須)
        <span class="error-php"><?php if ( isset( $error['email_check'] ) ) echo h( $error['email_check'] ); ?></span>
      </label>
      <input type="email"  class="form-control equal-to required" data-equal-to="email" data-error-equal-to="メールアドレスが異なります" id="email_check" name="email_check" placeholder="Email アドレス(確認用 必須)" value="<?php echo h($email_check); ?>">
    </div>
    <div class="form-group">
      <label for="tel">お電話番号(半角英数字)
        <span class="error-php"><?php if ( isset( $error['tel'] ) ) echo h( $error['tel'] ); ?></span>
      </label>
      <input type="tel" class="pattern form-control" data-pattern="tel" id="tel" name="tel" placeholder="お電話番号" data-error-pattern="電話番号の形式が正しくないようですのでご確認ください"  value="<?php echo h($tel); ?>">
    </div>
    <div class="form-group">
      <label for="subject">件名(必須)
        <span class="error-php"><?php if ( isset( $error['subject'] ) ) echo h( $error['subject'] ); ?></span>
      </label>
      <input type="text" class="required maxlength form-control" data-maxlength="100" id="subject" name="subject" placeholder="件名" value="<?php echo h($subject); ?>">
    </div>
    <div class="form-group">
      <label for="body">お問い合わせ内容(必須)
        <span class="error-php"><?php if ( isset( $error['body'] ) ) echo h( $error['body'] ); ?></span>
      </label>
      <textarea class="required maxlength showCount form-control" data-maxlength="1000" id="body" name="body" placeholder="お問い合わせ内容(1000文字まで)をお書きください" rows="3"><?php echo h($body); ?></textarea>
    </div>
    <button name="submitted" type="submit" class="btn btn-primary">送信</button>
  </form>
</div>
<!--  検証用の JavaScript(formValidation.js)の読み込み -->
<script src="formValidation.js"></script>
</body>
</html>

reCAPTCHA v2 を使う

Google が提供するキャプチャ認証システムの reCAPTCHA v2 を実装する例です。以下は自動返信の例に reCAPTCHA v2 を実装しています。

関連ページ:Google reCAPTCHA の使い方(v2/v3)

以下のサンプルでは実際にはメールは送信されません。

サンプルを別ページで開く

HTML

HTML では、reCAPTCHA の認証が通らなかった場合に PHP のエラーを表示する領域(4〜6行目)を追加します。

そして reCAPTCHA のウィジェットを表示する id 属性(id="recaptcha")を指定した div 要素を送信ボタンの前に配置します(41行目)。ウィジェットを表示する JavaScript ではこの要素の id を指定します。

HTML 抜粋
<body>
<div class="container">
  <h2 class="">お問い合わせフォーム</h2>
  <?php if (isset( $error['recaptcha'] )  ): ?>
  <p><span class="error"><?php echo h( $error['recaptcha'] ); ?></span></p>
  <?php endif; ?>
  <?php if (filter_input(INPUT_GET, 'result') ) : ?>
  <h4>送信完了!</h4>
  <p>送信完了いたしました。</p>
  <?php  if ( filter_input(INPUT_GET, 'result2', FILTER_VALIDATE_BOOLEAN) ) : ?>
    <p class="success">確認の自動返信メールをお送りいたしました。</p>
  <?php elseif ( !filter_input(INPUT_GET, 'result2', FILTER_VALIDATE_BOOLEAN) ): ?>
    <p class="fail">確認の自動返信メールが送信できませんでした。</p>
  <?php endif; ?>
  <hr>
  <?php elseif (isset($result) && !$result ): ?>
  <h4>送信失敗</h4>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>メール:<a href="mailto:info@example.com">Contact</a></p>
  <hr>
  <?php endif; ?>
  <p>以下のフォームからお問い合わせください。</p>
  <form id="form" class="validationForm" method="post" novalidate>
    <div class="form-group">
      <label for="name">お名前(必須)
        <span class="error-php"><?php if ( isset( $error['name'] ) ) echo h( $error['name'] ); ?></span>
      </label>
      <input type="text" class="required maxlength form-control" data-maxlength="30" id="name" name="name" placeholder="氏名" data-error-required="お名前は必須です。" value="<?php echo h($name); ?>">
    </div>

    ・・・中略・・・

    <div class="form-group">
      <label for="body">お問い合わせ内容(必須)
        <span class="error-php"><?php if ( isset( $error['body'] ) ) echo h( $error['body'] ); ?></span>
      </label>
      <textarea class="required maxlength showCount form-control" data-maxlength="1000" id="body" name="body" placeholder="お問い合わせ内容(1000文字まで)をお書きください" rows="3"><?php echo h($body); ?></textarea>
    </div>
      <!-- reCAPTCHA を表示する要素 -->
      <div id="recaptcha"></div>
    <button name="submitted" type="submit" class="btn btn-primary">送信</button>
  </form>
</div>
<!--  検証用の JavaScript(formValidation.js)の読み込み -->
<script src="formValidation.js"></script>

JavaScript

JavaScript を使って reCAPTCHA のウィジェットを表示し、ウィジェットにチェックを入れずに送信した場合にエラーを表示する検証を追加します。

grecaptcha.render() メソッドではウィジェットを表示する要素の id(4行目の recaptcha)とサイトキー(PHP で定義)などの必要なオプションやコールバック関数名を指定します。

そしてオプションで指定したコールバック関数(verifyCallback と expiredCallback)を定義します。

grecaptcha.render() メソッドに指定したコールバック関数 verifyCallback ではチェックが入った場合にウィジェット(id が recaptcha の要素)に verified というクラスを付与するように設定し、エラーメッセージを表示する要素の id は recaptcha_error としています。

また、ウィジェットを表示するための API の読み込みでは grecaptcha.render() を使ってウィジェットを表示するのでパラメータ onload=onloadCallback&render=explicit を指定しています(57行目)。

28〜54行目は、ウィジェットにチェックを入れずに送信した場合にエラーを表示する処理です。

送信時に id が recaptcha の要素(ウィジェット)に verified クラスが付与されていなければエラーを表示して送信を中止します。

関連項目:grecaptcha.render メソッドで表示

<script>
var onloadCallback = function() {
  //ウィジェットを表示するメソッド
  grecaptcha.render('recaptcha', {
    'sitekey' : "<?php echo $siteKey; ?>",  //サイトキー
    'callback' : verifyCallback, //コールバック関数の名前
    'expired-callback' : expiredCallback //コールバック関数の名前
  });
};
//チェックを入れて成功した場合に呼び出されるコールバック関数
var verifyCallback = function(response) {
  //ウィジェットの要素に verified クラスを設定
  document.getElementById('recaptcha').className = "verified";
  //エラーメッセージを表示する要素
  var recaptchaError = document.getElementById('recaptcha_error');
  if(recaptchaError != null) {
     //エラーメッセージを表示する要素が存在すれば削除
     recaptchaError.parentNode.removeChild(recaptchaError);
  }
};
//期限切れの場合に呼び出されるコールバック関数
var expiredCallback = function() {
  //verified クラスを削除
  document.getElementById('recaptcha').classList.remove('verified');
};

//validationForm クラスを指定した最初の form 要素を取得
const validationForm = document.getElementsByClassName('validationForm')[0];
//form の送信時に reCAPTCHA の状態を検証
validationForm.addEventListener('submit', (e) => {
  //id が recaptcha_error の要素(エラーメッセージの要素)を取得
  const errorElem = document.getElementById('recaptcha_error');
  //もし存在すれば削除してエラーをクリア(エラー表示が繰り返し追加されないように)
  if(errorElem) {
    errorElem.remove();
  }
  //id が recaptcha の要素を取得
  const recaptchaElem = document.getElementById('recaptcha');
  // verified クラスが付与されていなければエラーを表示して送信を中止
  if(!recaptchaElem.classList.contains('verified')) {
    //span 要素を生成
    const recaptchaErrorElem = document.createElement('p');
    //error クラスを設定
    recaptchaErrorElem.classList.add('error-js');
    //id 属性を設定
    recaptchaErrorElem.setAttribute('id', 'recaptcha_error');
    //エラーメッセージを設定
    recaptchaErrorElem.textContent = 'チェックを入れてください';
    //要素を挿入(追加)
    recaptchaElem.after(recaptchaErrorElem);
    //送信を中止
    e.preventDefault();
  }
});
</script>
<!--  ウィジェットを表示するための API の読み込み -->
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script> 

PHP

reCAPTCHA のサイトキーやシークレットキーは別ファイル(この例では recaptchavars.php)に保存して、外部からアクセスできないようにしておきます。

他のメール関連の情報を記述してあるファイル(この例では mailvars.php)にこれらの情報を記述しておくこともできます。

recaptchavars.php
<?php
// reCAPTCHA v2 サイトキー
define('V2_SITEKEY', 'xxxxxxxxxxxxxxxxxxxxxxx');
// reCAPTCHA v2 シークレットキー
define('V2_SECRETKEY', 'xxxxxxxxxxxxxxxxxxxxxxx');

上記ファイルを読み込み、サイトキーとシークレットキーの値を変数に代入しておきます(6〜10行目)。

18〜41行目が reCAPTCHA の認証部分で検証結果を変数 $result_status に代入します(40行目)。

認証に失敗した場合は検証結果に false が入っているので、その場合はエラーメッセージの配列($error)にエラーを追加します(46〜49行目)。

検証結果が false の場合、$error は空ではないので62行目の判定によりメールは送信されません。

関連項目:PHP を使った検証(v2)

php 抜粋
<?php
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';

//reCAPTCHA
require '../libs/recaptchavars.php';
// reCAPTCHA サイトキー
$siteKey = V2_SITEKEY;
// reCAPTCHA シークレットキー
$secretKey = V2_SECRETKEY;

・・・中略・・・

if (isset($_POST['submitted'])) {
  //POSTされたデータをチェック
  $_POST = checkInput( $_POST );

  if ( isset( $_POST[ 'g-recaptcha-response' ] ) ) {
    //cURL セッションを初期化
    $ch = curl_init();
    // curl_setopt() により転送時のオプションを設定
    //API の URL の指定
    curl_setopt($ch, CURLOPT_URL,"https://www.google.com/recaptcha/api/siteverify");
    //HTTP POST メソッドを使う
    curl_setopt($ch, CURLOPT_POST, true );
    //API パラメータの指定
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array(
      'secret' => $secretKey,
      'response' => $_POST[ 'g-recaptcha-response' ]
    )));
    //curl_execの返り値を文字列にする
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    //転送を実行してレスポンスを $json に格納
    $json = curl_exec($ch);
    //セッションを終了
    curl_close($ch);
    //レスポンスの $json(JSON形式)をデコード
    $rc_result = json_decode( $json );
    //reCAPTCHA 検証結果の真偽値
    $result_status = $rc_result->success ;
  }

  //エラーメッセージを保存する配列の初期化
  $error = array();

  //reCAPTCHA 検証
  if(!$result_status) {
    $error['recaptcha'] = 'reCAPTCHA 検証が失敗しました。申し訳ございませんがメールまたは電話にてご連絡ください。';
  }

  //値の検証
  if ( $name == '' ) {
    $error['name'] = '*お名前は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,30}\z/u', $name ) == 0 ) {
    $error['name'] = '*お名前は30文字以内でお願いします。';
  }

  ・・・中略・・・

  //エラーがなく且つ POST でのリクエストの場合
  if (empty($error) && $_SERVER['REQUEST_METHOD']==='POST') {
    //メールアドレス等を記述したファイルの読み込み
    require '../libs/mailvars.php';

    //メール本文の組み立て
    $mail_body = 'コンタクトページからのお問い合わせ' . "\n\n";
    $mail_body .=  date("Y年m月d日 D H時i分") . "\n\n";
    $mail_body .=  "お名前: " .h($name) . "\n";
    $mail_body .=  "Email: " . h($email) . "\n"  ;
    $mail_body .=  "お電話番号: " . h($tel) . "\n\n" ;
    $mail_body .=  "<お問い合わせ内容>" . "\n" . h($body);

    //--------sendmail------------
    //メールの宛先(名前<メールアドレス> の形式)。値は mailvars.php に記載
    $mailTo = mb_encode_mimeheader(MAIL_TO_NAME) ."<" . MAIL_TO. ">";

    ・・・以下省略・・・
  }
}
?>
<?php
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';
//reCAPTCHA
require '../libs/recaptchavars.php';
// reCAPTCHA サイトキー
$siteKey = V2_SITEKEY;
// reCAPTCHA シークレットキー
$secretKey = V2_SECRETKEY;

//お問い合わせ日時を日本時間に
date_default_timezone_set('Asia/Tokyo');

// 値が null であれば、空文字列に変換する関数
function nullToString($val) {
  if($val === null) return '';
  return $val;
}
//POSTされたデータの前後にあるホワイトスペースを削除してを変数に格納
$name = trim( nullToString(filter_input(INPUT_POST, 'name')) );
$email = trim( nullToString(filter_input(INPUT_POST, 'email')) );
$email_check = trim( nullToString(filter_input(INPUT_POST, 'email_check')) );
$tel = trim( nullToString(filter_input(INPUT_POST, 'tel')) );
$subject = trim( nullToString(filter_input(INPUT_POST, 'subject')) );
$body = trim( nullToString(filter_input(INPUT_POST, 'body')) );

if (isset($_POST['submitted'])) {

  //POSTされたデータをチェック
  $_POST = checkInput( $_POST );

  if ( isset( $_POST[ 'g-recaptcha-response' ] ) ) {
    //cURL セッションを初期化
    $ch = curl_init();
    // curl_setopt() により転送時のオプションを設定
    //API の URL の指定
    curl_setopt($ch, CURLOPT_URL,"https://www.google.com/recaptcha/api/siteverify");
    //HTTP POST メソッドを使う
    curl_setopt($ch, CURLOPT_POST, true );
    //API パラメータの指定
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array(
      'secret' => $secretKey,
      'response' => $_POST[ 'g-recaptcha-response' ]
    )));
    //curl_execの返り値を文字列にする
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    //転送を実行してレスポンスを $json に格納
    $json = curl_exec($ch);
    //セッションを終了
    curl_close($ch);
    //レスポンスの $json(JSON形式)をデコード
    $rc_result = json_decode( $json );
    //reCAPTCHA 検証結果の真偽値
    $result_status = $rc_result->success ;
  }
  //エラーメッセージを保存する配列の初期化
  $error = array();

  //reCAPTCHA 検証
  if(!$result_status) {
    $error['recaptcha'] = 'reCAPTCHA 検証が失敗しました。申し訳ございませんがメールまたは電話にてご連絡ください。';
  }

  //値の検証
  if ( $name == '' ) {
    $error['name'] = '*お名前は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,30}\z/u', $name ) == 0 ) {
    $error['name'] = '*お名前は30文字以内でお願いします。';
  }
  if ( $email == '' ) {
    $error['email'] = '*メールアドレスは必須です。';
  } else { //メールアドレスを正規表現でチェック
    $pattern = '/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/uiD';
    if ( !preg_match( $pattern, $email ) ) {
      $error['email'] = '*メールアドレスの形式が正しくありません。';
    }
  }
  if ( $email_check == '' ) {
    $error['email_check'] = '*確認用メールアドレスは必須です。';
  } else { //メールアドレスを正規表現でチェック
    if ( $email_check !== $email ) {
      $error['email_check'] = '*メールアドレスが一致しません。';
    }
  }
  if ( preg_match( '/\A[[:^cntrl:]]{0,30}\z/u', $tel ) == 0 ) {
    $error['tel'] = '*電話番号は30文字以内でお願いします。';
  }
  if ( $tel != '' && preg_match( '/\A\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}\z/u', $tel ) == 0 ) {
    $error['tel_format'] = '*電話番号の形式が正しくありません。';
  }
  if ( $subject == '' ) {
    $error['subject'] = '*件名は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,100}\z/u', $subject ) == 0 ) {
    $error['subject'] = '*件名は100文字以内でお願いします。';
  }
  if ( $body == '' ) {
    $error['body'] = '*内容は必須項目です。';
    //制御文字(タブ、復帰、改行を除く)でないことと文字数をチェック
  } else if ( preg_match( '/\A[\r\n\t[:^cntrl:]]{1,1050}\z/u', $body ) == 0 ) {
    $error['body'] = '*内容は1000文字以内でお願いします。';
  }

  //エラーがなく且つ POST でのリクエストの場合
  if (empty($error) && $_SERVER['REQUEST_METHOD']==='POST') {
    //メールアドレス等を記述したファイルの読み込み
    require '../libs/mailvars.php';

    //メール本文の組み立て
    $mail_body = 'コンタクトページからのお問い合わせ' . "\n\n";
    $mail_body .=  date("Y年m月d日 D H時i分") . "\n\n";
    $mail_body .=  "お名前: " .h($name) . "\n";
    $mail_body .=  "Email: " . h($email) . "\n"  ;
    $mail_body .=  "お電話番号: " . h($tel) . "\n\n" ;
    $mail_body .=  "<お問い合わせ内容>" . "\n" . h($body);

    //--------sendmail------------
    //メールの宛先(名前<メールアドレス> の形式)。値は mailvars.php に記載
    $mailTo = mb_encode_mimeheader(MAIL_TO_NAME) ."<" . MAIL_TO. ">";

    //Return-Pathに指定するメールアドレス
    $returnMail = MAIL_RETURN_PATH; //
    //mbstringの日本語設定
    mb_language( 'ja' );
    mb_internal_encoding( 'UTF-8' );

    // 送信者情報(From ヘッダー)の設定
    $header = "From: " . mb_encode_mimeheader($name) ."<" . $email. ">\n";
    $header .= "Cc: " . mb_encode_mimeheader(MAIL_CC_NAME) ."<" . MAIL_CC.">\n";
    $header .= "Bcc: <" . MAIL_BCC.">";

    //メールの送信
    //セーフモードがOnの場合は第5引数が使えない
    if ( ini_get( 'safe_mode' ) ) {
      $result = mb_send_mail( $mailTo, $subject, $mail_body, $header );
    } else {
      $result = mb_send_mail( $mailTo, $subject, $mail_body, $header, '-f' . $returnMail );
    }

    //メール送信の結果判定
    if ( $result ) {
      //自動返信メール
      //ヘッダー情報
      $ar_header = "MIME-Version: 1.0\n";
      $ar_header .= "From: " . mb_encode_mimeheader( AUTO_REPLY_NAME ) . " <" . MAIL_TO . ">\n";
      $ar_header .= "Reply-To: " . mb_encode_mimeheader( AUTO_REPLY_NAME ) . " <" . MAIL_TO . ">\n";
      //件名
      $ar_subject = 'お問い合わせ自動返信メール';
      //本文
      $ar_body = $name." 様\n\n";
      $ar_body .= "この度は、お問い合わせ頂き誠にありがとうございます。" . "\n\n";
      $ar_body .= "下記の内容でお問い合わせを受け付けました。\n\n";
      $ar_body .= "お問い合わせ日時:" . date("Y-m-d H:i") . "\n";
      $ar_body .= "お名前:" . $name . "\n";
      $ar_body .= "メールアドレス:" . $email . "\n";
      $ar_body .= "お電話番号: " . $tel . "\n\n" ;
      $ar_body .="<お問い合わせ内容>" . "\n" . $body;

      //メールの送信結果を変数に代入 (サンプルなのでコメントアウト)
      if ( ini_get( 'safe_mode' ) ) {
        //セーフモードがOnの場合は第5引数が使えない
        $result2 = mb_send_mail( $email, $ar_subject, $ar_body , $ar_header  );
      } else {
        $result2 = mb_send_mail( $email, $ar_subject, $ar_body , $ar_header , '-f' . $returnMail );
      }

      $_POST = array(); //空の配列を代入し、すべてのPOST変数を消去
      //変数の値も初期化
      $name = '';
      $email = '';
      $email_check = '';
      $tel = '';
      $subject = '';
      $body = '';

      //再読み込みによる二重送信の防止
      $params = '?result='. $result .'&result2=' . $result2;
      //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
      if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
        $_SERVER['HTTPS'] = 'on';
      }
      $url = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://').$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];
      header('Location:' . $url . $params);
      exit;
    }
  }
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>コンタクトフォーム</title>
<link href="../bootstrap.min.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="container">
  <h2 class="">お問い合わせフォーム</h2>
  <?php if (isset( $error['recaptcha'] )  ): ?>
  <p><span class="error"><?php echo h( $error['recaptcha'] ); ?></span></p>
  <?php endif; ?>
  <?php if (filter_input(INPUT_GET, 'result') ) : ?>
  <h4>送信完了!</h4>
  <p>送信完了いたしました。</p>
  <?php  if ( filter_input(INPUT_GET, 'result2', FILTER_VALIDATE_BOOLEAN) ) : ?>
    <p class="success">確認の自動返信メールをお送りいたしました。</p>
  <?php elseif ( !filter_input(INPUT_GET, 'result2', FILTER_VALIDATE_BOOLEAN) ): ?>
    <p class="fail">確認の自動返信メールが送信できませんでした。</p>
  <?php endif; ?>
  <hr>
  <?php elseif (isset($result) && !$result ): ?>
  <h4>送信失敗</h4>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>メール:<a href="mailto:info@example.com">Contact</a></p>
  <hr>
  <?php endif; ?>
  <p>以下のフォームからお問い合わせください。</p>
  <form id="form" class="validationForm" method="post" novalidate>
    <div class="form-group">
      <label for="name">お名前(必須)
        <span class="error-php"><?php if ( isset( $error['name'] ) ) echo h( $error['name'] ); ?></span>
      </label>
      <input type="text" class="required maxlength form-control" data-maxlength="30" id="name" name="name" placeholder="氏名" data-error-required="お名前は必須です。" value="<?php echo h($name); ?>">
    </div>
    <div class="form-group">
      <label for="email">Email(必須)
        <span class="error-php"><?php if ( isset( $error['email'] ) ) echo h( $error['email'] ); ?></span>
      </label>
      <input type="email" class="required pattern form-control" data-pattern="email" id="email" name="email" placeholder="Email アドレス" data-error-required="Email アドレスは必須です。"  data-error-pattern="Email の形式が正しくないようですのでご確認ください" value="<?php echo h($email); ?>">
    </div>
    <div class="form-group">
      <label for="email_check">Email(確認用 必須)
        <span class="error-php"><?php if ( isset( $error['email_check'] ) ) echo h( $error['email_check'] ); ?></span>
      </label>
      <input type="email"  class="form-control equal-to required" data-equal-to="email" data-error-equal-to="メールアドレスが異なります" id="email_check" name="email_check" placeholder="Email アドレス(確認用 必須)" value="<?php echo h($email_check); ?>">
    </div>
    <div class="form-group">
      <label for="tel">お電話番号(半角英数字)
        <span class="error-php"><?php if ( isset( $error['tel'] ) ) echo h( $error['tel'] ); ?></span>
      </label>
      <input type="tel" class="pattern form-control" data-pattern="tel" id="tel" name="tel" placeholder="お電話番号" data-error-pattern="電話番号の形式が正しくないようですのでご確認ください"  value="<?php echo h($tel); ?>">
    </div>
    <div class="form-group">
      <label for="subject">件名(必須)
        <span class="error-php"><?php if ( isset( $error['subject'] ) ) echo h( $error['subject'] ); ?></span>
      </label>
      <input type="text" class="required maxlength form-control" data-maxlength="100" id="subject" name="subject" placeholder="件名" value="<?php echo h($subject); ?>">
    </div>
    <div class="form-group">
      <label for="body">お問い合わせ内容(必須)
        <span class="error-php"><?php if ( isset( $error['body'] ) ) echo h( $error['body'] ); ?></span>
      </label>
      <textarea class="required maxlength showCount form-control" data-maxlength="1000" id="body" name="body" placeholder="お問い合わせ内容(1000文字まで)をお書きください" rows="3"><?php echo h($body); ?></textarea>
    </div>
      <div id="recaptcha" style="margin-bottom: 20px;"></div><!-- reCAPTCHA を表示する要素 -->
    <button name="submitted" type="submit" class="btn btn-primary">送信</button>
  </form>
</div>
<!--  検証用の JavaScript(formValidation.js)の読み込み -->
<script src="formValidation.js"></script>
<script>
var onloadCallback = function() {
  //ウィジェットを表示するメソッド
  grecaptcha.render('recaptcha', {
    'sitekey' : "<?php echo $siteKey; ?>",  //サイトキー
    'callback' : verifyCallback, //コールバック関数の名前
    'expired-callback' : expiredCallback //コールバック関数の名前
  });
};
//チェックを入れて成功した場合に呼び出されるコールバック関数
var verifyCallback = function(response) {
  //ウィジェットの要素に verified クラスを設定
  document.getElementById('recaptcha').className = "verified";
  //エラーメッセージを表示する要素
  var recaptchaError = document.getElementById('recaptcha_error');
  if(recaptchaError != null) {
     //エラーメッセージを表示する要素が存在すれば削除
     recaptchaError.parentNode.removeChild(recaptchaError);
  }
};
//期限切れの場合に呼び出されるコールバック関数
var expiredCallback = function() {
  //verified クラスを削除
  document.getElementById('recaptcha').classList.remove('verified');
};

//validationForm クラスを指定した最初の form 要素を取得
const validationForm = document.getElementsByClassName('validationForm')[0];
validationForm.addEventListener('submit', (e) => {
  //id が recaptcha_error の要素(エラーメッセージの要素)を取得
  const errorElem = document.getElementById('recaptcha_error');
  //もし存在すれば削除してエラーをクリア
  if(errorElem) {
    errorElem.remove();
  }
  //id が recaptcha の要素を取得
  const recaptchaElem = document.getElementById('recaptcha');
  // verified クラスが付与されていなければエラーを表示して送信を中止
  if(!recaptchaElem.classList.contains('verified')) {
    //span 要素を生成
    const recaptchaErrorElem = document.createElement('p');
    //error クラスを設定
    recaptchaErrorElem.classList.add('error-js');
    //id 属性を設定
    recaptchaErrorElem.setAttribute('id', 'recaptcha_error');
    //エラーメッセージを設定
    recaptchaErrorElem.textContent = 'チェックを入れてください';
    //要素を挿入(追加)
    recaptchaElem.after(recaptchaErrorElem);
    //送信を中止
    e.preventDefault();
  }
});
</script>
<script src="https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit" async defer></script>
</body>
</html>

reCAPTCHA v3 を使う

reCAPTCHA v3 を実装する例です。以下は自動返信の例に reCAPTCHA v3 を実装しています。

以下のサンプルでは実際にはメールは送信されません。

サンプルを別ページで開く

HTML

HTML では、reCAPTCHA の認証が通らなかった場合にエラーを表示する領域(23〜25行目)を追加します(通常のお問い合わせはエラーになることはほとんどないと思います)。

3〜21行目は reCAPTCHA の動作確認(テスト)用の表示の領域で reCAPTCHA のレスポンスの内容を表示しています(※ 実際のコンタクトフォームでは記述しません)。

メールの送信が成功した場合には PHP の記述により GET メソッド(パラメータ経由)で reCAPTCHA のレスポンスの内容が送信されるので、filter_input() で取得し除去フィルタ(FILTER_SANITIZE_FULL_SPECIAL_CHARS)でサニタイズした値を出力しています。送信に失敗した場合は $rc_result に格納されているレスポンスを表示しています。

<body>
<div class="container">
<!-- ここから reCAPTCHA 検証結果確認用(テスト用)  -->
<div class="success">
<!-- reCAPTCHA 検証を通過した場合  -->
<?php
$get_success=filter_input(INPUT_GET,'success',FILTER_SANITIZE_FULL_SPECIAL_CHARS) ;
$get_action=filter_input(INPUT_GET,'action',FILTER_SANITIZE_FULL_SPECIAL_CHARS) ;
$get_score=filter_input(INPUT_GET,'score',FILTER_SANITIZE_FULL_SPECIAL_CHARS) ;
if ($get_success) echo 'success: '. $get_success .'<br>';
if ($get_action) echo 'action: '. $get_action .'<br>';
if ($get_score) echo 'score: '. $get_score .'<br>';
?>
</div>
<div class="fail">
<!-- reCAPTCHA 検証を失敗した場合  -->
<?php if(isset($rc_result->success)) echo 'success:'.h($rc_result->success).'<br>' ?>
<?php if(isset($rc_result->action)) echo 'action:'.h($rc_result->action).'<br>' ?>
<?php if(isset($rc_result->score)) echo 'score:'.h($rc_result->score).'<br>' ?>
</div>
<!-- ここまで reCAPTCHA 検証結果確認用(テスト用) -->
<h2 class="">お問い合わせフォーム</h2>
<?php if (isset( $error['recaptcha'] ) ): ?><!-- ここから追加 -->
<p><span class="error"><?php echo h( $error['recaptcha'] ); ?></span></p>
<?php endif; ?><!-- ここまで追加 -->
<?php if (filter_input(INPUT_GET, 'result') ) : ?>
<h4>送信完了!</h4>
・・・以下省略・・・

JavaScript

入力値の検証用の JavaScript と reCAPTCHA を表示するためサイトキーをパラメータに指定した API を読み込みます。

入力値の検証では formValidation.js により、エラーがあれば(検証を通過しなければ) error-js クラスが付与された要素が生成されます。

form 要素に submit イベントを設定し、grecaptcha.execute() メソッドでトークン(token)を非同期的に取得して、エラーがなければ input 要素を生成し必要な値(トークンとアクション名)を設定し submit() でフォームを送信しています。

トークンの有効期限は送信ボタンをクリックしてから約2分間になります。

詳細:reCAPTCHA v3 クライアント側の実装例

<!--  検証用の JavaScript(formValidation.js)の読み込み -->
<script src="formValidation.js"></script>
<!--  reCAPTCHA を表示するための API の読み込み -->
<script src="https://www.google.com/recaptcha/api.js?render=<?php echo $siteKey; ?>"></script>
<script>
//validationForm クラスを指定した最初の form 要素を取得
const validationForm = document.getElementsByClassName('validationForm')[0];
//フォーム要素に submit イベントハンドラを設定
validationForm.addEventListener('submit', (e) => {
  //デフォルトの動作(送信)を停止
  e.preventDefault();
  const action_name = 'contact'; //アクション名
  //トークンを取得
  grecaptcha.ready(function() {
    grecaptcha.execute('<?php echo $siteKey; ?>', {action: action_name}).then(function(token) {
      //エラー(error-js クラス)の要素を取得
      const errorElem = document.querySelector('.error-js');
      //エラー(error-js クラス)の要素が存在しなければ
      if(errorElem === null) {
        const token_input = document.createElement('input'); //input 要素を生成
        token_input.type = 'hidden';
        token_input.name = 'g-recaptcha-response';
        token_input.value = token; //トークンを値に設定
        validationForm.appendChild(token_input);
        const action_input = document.createElement('input'); //input 要素を生成
        action_input.type = 'hidden';
        action_input.name = 'action';
        action_input.value = action_name;  //アクション名を値に設定
        validationForm.appendChild(action_input);
        validationForm.submit();  //フォームを送信
      }
    });
  });
});
</script>

PHP

他の例では、送信ボタンがクリックされ $_POST['submitted']に値が設定されたら処理を実行するようにしていますが、この場合は reCAPTCHA のトークンの値($_POST['g-recaptcha-response'])が取得できていれば処理を実行します(32行目)。

そしてトークンとアクション名が取得できれば API を使ってトークンを検証します(38〜67行目)。

v2 とは異なりレスポンスの success の値だけでは判定できないので、success が true でアクション名が一致し、スコアが 0.5 以上の場合に合格(検証通過)としています。スコアの値を変更することで、判定の厳しさの度合いを設定することができます(60行目)。

この例では検証の判定結果を変数 $result_status に真偽値で格納し(60〜66行目)、$result_status が false の場合は、エラーとして $error['recaptcha'] にエラーメッセージを設定しています(73〜75行目)。

95行目の条件文でエラーがなければ送信処理を実行するようにしています。

詳細:reCAPTCHA v3 PHP を使った検証

<?php
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';

//reCAPTCHA
require '../libs/recaptchavars.php';
// reCAPTCHA サイトキー
$siteKey = V3_SITEKEY;
// reCAPTCHA シークレットキー
$secretKey = V3_SECRETKEY;

//お問い合わせ日時を日本時間に
date_default_timezone_set('Asia/Tokyo');

//POSTされたデータを変数に格納(値の初期化とデータの整形:前後にあるホワイトスペースを削除)
$name = trim( filter_input(INPUT_POST, 'name') );
$email = trim( filter_input(INPUT_POST, 'email') );
$email_check = trim( filter_input(INPUT_POST, 'email_check') );
$tel = trim( filter_input(INPUT_POST, 'tel') );
$subject = trim( filter_input(INPUT_POST, 'subject'));
$body = trim( filter_input(INPUT_POST, 'body') );

//reCAPTCHA トークン
$token = filter_input(INPUT_POST, 'g-recaptcha-response');
//reCAPTCHA アクション名
$action = filter_input(INPUT_POST, 'action');

//reCAPTCHA の検証を通過したかどうかの真偽値
$result_status = false;  //初期値を false に

//★トークン($_POST['g-recaptcha-response'])が設定されていれば以下を実行
if (isset($_POST['g-recaptcha-response'])) {

  //POSTされたデータをチェック
  $_POST = checkInput( $_POST );

  // トークンとアクション名が取得できれば
  if ( $token && $action) {
    //cURL セッションを初期化(API のレスポンスの取得)
    $ch = curl_init();
    // curl_setopt() により転送時のオプションを設定
    //URL の指定
    curl_setopt($ch, CURLOPT_URL,"https://www.google.com/recaptcha/api/siteverify");
    //HTTP POST メソッドを使う
    curl_setopt($ch, CURLOPT_POST, true );
    //API パラメータの指定
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array(
      'secret' => $secretKey,
      'response' => $token
    )));
    //curl_execの返り値を文字列にする
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    //転送を実行してレスポンスを $api_response に格納
    $api_response = curl_exec($ch);
    //セッションを終了
    curl_close($ch);
    //レスポンスの $json(JSON形式)をデコード
    $rc_result = json_decode( $api_response );
    //判定(必要に応じて score >= 0.5 の値を変更)
    if ( $rc_result->success && $rc_result->action === $action && $rc_result->score >= 0.5) {
      //success が true でアクション名が一致し、スコアが 0.5 以上の場合は合格
      $result_status = true;
    } else {
      // 上記以外の場合は 不合格
      $result_status = false;
    }
  }

  //エラーメッセージを保存する配列の初期化
  $error = array();

  //reCAPTCHA 検証(判定結果 $result_status が false ならエラーを表示)
  if(!$result_status) {
    $error['recaptcha'] = 'reCAPTCHA 検証が失敗しました。';
  }

  //値の検証
  if ( $name == '' ) {
    $error['name'] = '*お名前は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,30}\z/u', $name ) == 0 ) {
    $error['name'] = '*お名前は30文字以内でお願いします。';
  }

  ・・・中略・・・

  if ( $body == '' ) {
    $error['body'] = '*内容は必須項目です。';
    //制御文字(タブ、復帰、改行を除く)でないことと文字数をチェック
  } else if ( preg_match( '/\A[\r\n\t[:^cntrl:]]{1,1050}\z/u', $body ) == 0 ) {
    $error['body'] = '*内容は1000文字以内でお願いします。';
  }

  //エラーがなく且つ POST でのリクエストの場合
  if (empty($error) && $_SERVER['REQUEST_METHOD']==='POST') {
    //メールアドレス等を記述したファイルの読み込み
    require '../libs/mailvars.php';

    //メール本文の組み立て
    $mail_body = 'コンタクトページからのお問い合わせ' . "\n\n";
    $mail_body .=  date("Y年m月d日 D H時i分") . "\n\n";
    $mail_body .=  "お名前: " .h($name) . "\n";
    $mail_body .=  "Email: " . h($email) . "\n"  ;
    $mail_body .=  "お電話番号: " . h($tel) . "\n\n" ;
    $mail_body .=  "<お問い合わせ内容>" . "\n" . h($body);

    //--------sendmail------------

    //メールの宛先(名前<メールアドレス> の形式)。値は mailvars.php に記載
    $mailTo = mb_encode_mimeheader(MAIL_TO_NAME) ."<" . MAIL_TO. ">";

    ・・・以下省略・・・
    }
  }
}
?>
<?php
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';

//reCAPTCHA
require '../libs/recaptchavars.php';
// reCAPTCHA サイトキー
$siteKey = V3_SITEKEY;
// reCAPTCHA シークレットキー
$secretKey = V3_SECRETKEY;

//お問い合わせ日時を日本時間に
date_default_timezone_set('Asia/Tokyo');

// 値が null であれば、空文字列に変換する関数
function nullToString($val) {
  if($val === null) return '';
  return $val;
}
//POSTされたデータの前後にあるホワイトスペースを削除してを変数に格納
$name = trim( nullToString(filter_input(INPUT_POST, 'name')) );
$email = trim( nullToString(filter_input(INPUT_POST, 'email')) );
$email_check = trim( nullToString(filter_input(INPUT_POST, 'email_check')) );
$tel = trim( nullToString(filter_input(INPUT_POST, 'tel')) );
$subject = trim( nullToString(filter_input(INPUT_POST, 'subject')) );
$body = trim( nullToString(filter_input(INPUT_POST, 'body')) );

//reCAPTCHA トークン
$token = filter_input(INPUT_POST, 'g-recaptcha-response');
//reCAPTCHA アクション名
$action = filter_input(INPUT_POST, 'action');

//reCAPTCHA の検証を通過したかどうかの真偽値
$result_status = false;

//★トークン($_POST['g-recaptcha-response'])が設定されていれば以下を実行
if (isset($_POST['g-recaptcha-response'])) {
  //POSTされたデータをチェック
  $_POST = checkInput( $_POST );
  // トークンとアクション名が取得できれば
  if ( $token && $action) {
    //cURL セッションを初期化(API のレスポンスの取得)
    $ch = curl_init();
    // curl_setopt() により転送時のオプションを設定
    //URL の指定
    curl_setopt($ch, CURLOPT_URL,"https://www.google.com/recaptcha/api/siteverify");
    //HTTP POST メソッドを使う
    curl_setopt($ch, CURLOPT_POST, true );
    //API パラメータの指定
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array(
      'secret' => $secretKey,
      'response' => $token
    )));
    //curl_execの返り値を文字列にする
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    //転送を実行してレスポンスを $api_response に格納
    $api_response = curl_exec($ch);
    //セッションを終了
    curl_close($ch);
    //レスポンスの $json(JSON形式)をデコード
    $rc_result = json_decode( $api_response );
    //判定
    if ( $rc_result->success && $rc_result->action === $action && $rc_result->score >= 0.5) {
      //success が true でアクション名が一致し、スコアが 0.5 以上の場合は合格
      $result_status = true;
    } else {
      // 上記以外の場合は 不合格
      $result_status = false;
    }
  }
  //エラーメッセージを保存する配列の初期化
  $error = array();
  //reCAPTCHA 検証(判定結果 $result_status が false ならエラーを表示)
  if(!$result_status) {
    $error['recaptcha'] = 'reCAPTCHA 検証が失敗しました。';
  }

  //値の検証
  if ( $name == '' ) {
    $error['name'] = '*お名前は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,30}\z/u', $name ) == 0 ) {
    $error['name'] = '*お名前は30文字以内でお願いします。';
  }
  if ( $email == '' ) {
    $error['email'] = '*メールアドレスは必須です。';
  } else { //メールアドレスを正規表現でチェック
    $pattern = '/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/uiD';
    if ( !preg_match( $pattern, $email ) ) {
      $error['email'] = '*メールアドレスの形式が正しくありません。';
    }
  }
  if ( $email_check == '' ) {
    $error['email_check'] = '*確認用メールアドレスは必須です。';
  } else { //メールアドレスを正規表現でチェック
    if ( $email_check !== $email ) {
      $error['email_check'] = '*メールアドレスが一致しません。';
    }
  }
  if ( preg_match( '/\A[[:^cntrl:]]{0,30}\z/u', $tel ) == 0 ) {
    $error['tel'] = '*電話番号は30文字以内でお願いします。';
  }
  if ( $tel != '' && preg_match( '/\A\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}\z/u', $tel ) == 0 ) {
    $error['tel_format'] = '*電話番号の形式が正しくありません。';
  }
  if ( $subject == '' ) {
    $error['subject'] = '*件名は必須項目です。';
    //制御文字でないことと文字数をチェック
  } else if ( preg_match( '/\A[[:^cntrl:]]{1,100}\z/u', $subject ) == 0 ) {
    $error['subject'] = '*件名は100文字以内でお願いします。';
  }
  if ( $body == '' ) {
    $error['body'] = '*内容は必須項目です。';
    //制御文字(タブ、復帰、改行を除く)でないことと文字数をチェック
  } else if ( preg_match( '/\A[\r\n\t[:^cntrl:]]{1,1050}\z/u', $body ) == 0 ) {
    $error['body'] = '*内容は1000文字以内でお願いします。';
  }

  //エラーがなく且つ POST でのリクエストの場合
  if (empty($error) && $_SERVER['REQUEST_METHOD']==='POST') {
    //メールアドレス等を記述したファイルの読み込み
    require '../libs/mailvars.php';

    //メール本文の組み立て
    $mail_body = 'コンタクトページからのお問い合わせ' . "\n\n";
    $mail_body .=  date("Y年m月d日 D H時i分") . "\n\n";
    $mail_body .=  "お名前: " .h($name) . "\n";
    $mail_body .=  "Email: " . h($email) . "\n"  ;
    $mail_body .=  "お電話番号: " . h($tel) . "\n\n" ;
    $mail_body .=  "<お問い合わせ内容>" . "\n" . h($body);

    //--------sendmail------------
    //メールの宛先(名前<メールアドレス> の形式)。値は mailvars.php に記載
    $mailTo = mb_encode_mimeheader(MAIL_TO_NAME) ."<" . MAIL_TO. ">";
    //Return-Pathに指定するメールアドレス
    $returnMail = MAIL_RETURN_PATH; //
    //mbstringの日本語設定
    mb_language( 'ja' );
    mb_internal_encoding( 'UTF-8' );
    // 送信者情報(From ヘッダー)の設定
    $header = "From: " . mb_encode_mimeheader($name) ."<" . $email. ">\n";
    $header .= "Cc: " . mb_encode_mimeheader(MAIL_CC_NAME) ."<" . MAIL_CC.">\n";
    $header .= "Bcc: <" . MAIL_BCC.">";
    //メールの送信
    //セーフモードがOnの場合は第5引数が使えない(サンプルなので送信しないようにコメントアウト)
    if ( ini_get( 'safe_mode' ) ) {
      $result = mb_send_mail( $mailTo, $subject, $mail_body, $header );
    } else {
      $result = mb_send_mail( $mailTo, $subject, $mail_body, $header, '-f' . $returnMail );
    }
    //メール送信の結果判定
    if ( $result ) {
      //自動返信メール
      //ヘッダー情報
      $ar_header = "MIME-Version: 1.0\n";
      $ar_header .= "From: " . mb_encode_mimeheader( AUTO_REPLY_NAME ) . " <" . MAIL_TO . ">\n";
      $ar_header .= "Reply-To: " . mb_encode_mimeheader( AUTO_REPLY_NAME ) . " <" . MAIL_TO . ">\n";
      //件名
      $ar_subject = 'お問い合わせ自動返信メール';
      //本文
      $ar_body = $name." 様\n\n";
      $ar_body .= "この度は、お問い合わせ頂き誠にありがとうございます。" . "\n\n";
      $ar_body .= "下記の内容でお問い合わせを受け付けました。\n\n";
      $ar_body .= "お問い合わせ日時:" . date("Y-m-d H:i") . "\n";
      $ar_body .= "お名前:" . $name . "\n";
      $ar_body .= "メールアドレス:" . $email . "\n";
      $ar_body .= "お電話番号: " . $tel . "\n\n" ;
      $ar_body .="<お問い合わせ内容>" . "\n" . $body;
      //メールの送信結果を変数に代入 (サンプルなので送信しないようにコメントアウト)
      if ( ini_get( 'safe_mode' ) ) {
        //セーフモードがOnの場合は第5引数が使えない
        $result2 = mb_send_mail( $email, $ar_subject, $ar_body , $ar_header  );
      } else {
        $result2 = mb_send_mail( $email, $ar_subject, $ar_body , $ar_header , '-f' . $returnMail );
      }
      $_POST = array(); //空の配列を代入し、すべてのPOST変数を消去
      //変数の値も初期化
      $name = '';
      $email = '';
      $email_check = '';
      $tel = '';
      $subject = '';
      $body = '';
      //再読み込みによる二重送信の防止
      $params = '?result='. $result .'&result2=' . $result2;
      //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
      if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
        $_SERVER['HTTPS'] = 'on';
      }
      //reCAPTCHA 検証結果確認用パラメータ
      $params .= '&success=' . $rc_result->success  .'&action=' . $rc_result->action .'&score=' . $rc_result->score;
      $url = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://').$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'];
      header('Location:' . $url . $params);
      exit;
    }
  }
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>コンタクトフォーム</title>
<link href="../../../../plugins/bootstrap4/css/bootstrap.min.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="container">
  <!-- ここから reCAPTCHA 検証結果確認用(テスト用)  -->
  <div class="success">
  <!-- reCAPTCHA 検証を通過した場合  -->
  <?php
  $get_success = filter_input(INPUT_GET, 'success', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ;
  if ($get_success) echo 'success: '. $get_success .'<br>';
  $get_action = filter_input(INPUT_GET, 'action', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ;
  if ($get_action) echo 'action: '. $get_action .'<br>';
  $get_score = filter_input(INPUT_GET, 'score', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ;
  if ($get_score) echo 'score: '. $get_score .'<br>';
  ?>
  </div>
  <div class="fail">
  <!-- reCAPTCHA 検証を失敗した場合  -->
  <?php if (isset($rc_result->success)) echo 'success: '. h($rc_result->success).'<br>' ?>
  <?php if (isset($rc_result->action)) echo 'action: '. h($rc_result->action).'<br>' ?>
  <?php if (isset($rc_result->score)) echo 'score: '.  h($rc_result->score).'<br>' ?>
  </div>
  <!-- ここまで reCAPTCHA 検証結果確認用(テスト用) -->
  <h2 class="">お問い合わせフォーム</h2>
  <?php if (isset( $error['recaptcha'] ) ): ?>
  <p><span class="error"><?php echo h( $error['recaptcha'] ); ?></span></p>
  <?php endif; ?>
  <?php if (filter_input(INPUT_GET, 'result') ) : ?>
  <h4>送信完了!</h4>
  <p>送信完了いたしました。</p>
  <?php  if ( filter_input(INPUT_GET, 'result2', FILTER_VALIDATE_BOOLEAN) ) : ?>
    <p class="success">確認の自動返信メールをお送りいたしました。</p>
  <?php elseif ( !filter_input(INPUT_GET, 'result2', FILTER_VALIDATE_BOOLEAN) ): ?>
    <p class="fail">確認の自動返信メールが送信できませんでした。</p>
  <?php endif; ?>
  <hr>
  <?php elseif (isset($result) && !$result ): ?>
  <h4>送信失敗</h4>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>メール:<a href="mailto:info@example.com">Contact</a></p>
  <hr>
  <?php endif; ?>
  <p>以下のフォームからお問い合わせください。</p>
  <form id="form" class="validationForm" method="post" novalidate>
    <div class="form-group">
      <label for="name">お名前(必須)
        <span class="error-php"><?php if ( isset( $error['name'] ) ) echo h( $error['name'] ); ?></span>
      </label>
      <input type="text" class="required maxlength form-control" data-maxlength="30" id="name" name="name" placeholder="氏名" data-error-required="お名前は必須です。" value="<?php echo h($name); ?>">
    </div>
    <div class="form-group">
      <label for="email">Email(必須)
        <span class="error-php"><?php if ( isset( $error['email'] ) ) echo h( $error['email'] ); ?></span>
      </label>
      <input type="email" class="required pattern form-control" data-pattern="email" id="email" name="email" placeholder="Email アドレス" data-error-required="Email アドレスは必須です。"  data-error-pattern="Email の形式が正しくないようですのでご確認ください" value="<?php echo h($email); ?>">
    </div>
    <div class="form-group">
      <label for="email_check">Email(確認用 必須)
        <span class="error-php"><?php if ( isset( $error['email_check'] ) ) echo h( $error['email_check'] ); ?></span>
      </label>
      <input type="email"  class="form-control equal-to required" data-equal-to="email" data-error-equal-to="メールアドレスが異なります" id="email_check" name="email_check" placeholder="Email アドレス(確認用 必須)" value="<?php echo h($email_check); ?>">
    </div>
    <div class="form-group">
      <label for="tel">お電話番号(半角英数字)
        <span class="error-php"><?php if ( isset( $error['tel'] ) ) echo h( $error['tel'] ); ?></span>
      </label>
      <input type="tel" class="pattern form-control" data-pattern="tel" id="tel" name="tel" placeholder="お電話番号" data-error-pattern="電話番号の形式が正しくないようですのでご確認ください"  value="<?php echo h($tel); ?>">
    </div>
    <div class="form-group">
      <label for="subject">件名(必須)
        <span class="error-php"><?php if ( isset( $error['subject'] ) ) echo h( $error['subject'] ); ?></span>
      </label>
      <input type="text" class="required maxlength form-control" data-maxlength="100" id="subject" name="subject" placeholder="件名" value="<?php echo h($subject); ?>">
    </div>
    <div class="form-group">
      <label for="body">お問い合わせ内容(必須)
        <span class="error-php"><?php if ( isset( $error['body'] ) ) echo h( $error['body'] ); ?></span>
      </label>
      <textarea class="required maxlength showCount form-control" data-maxlength="1000" id="body" name="body" placeholder="お問い合わせ内容(1000文字まで)をお書きください" rows="3"><?php echo h($body); ?></textarea>
    </div>
    <button name="submitted" type="submit" class="btn btn-primary">送信</button>
    <p style="margin-top: 30px;">※サンプルですので実際にはメールは送信されません</p>
  </form>
</div>
<!--  検証用の JavaScript(formValidation.js)の読み込み -->
<script src="formValidation.js"></script>
<script src="https://www.google.com/recaptcha/api.js?render=<?php echo $siteKey; ?>"></script>
<script>
//validationForm クラスを指定した最初の form 要素を取得
const validationForm = document.getElementsByClassName('validationForm')[0];
//フォーム要素にイベントハンドラを設定
validationForm.addEventListener('submit', (e) => {
  //デフォルトの動作(送信)を停止
  e.preventDefault();
  const action_name = 'contact'; //アクション名
  //トークンを取得
  grecaptcha.ready(function() {
    grecaptcha.execute('<?php echo $siteKey; ?>', {action: action_name}).then(function(token) {
      //エラー(error-js クラス)の要素を取得
      const errorElem = document.querySelector('.error-js');
      //エラー(error-js クラス)の要素が存在しなければ
      if(errorElem === null) {
        const token_input = document.createElement('input'); //input 要素を生成
        token_input.type = 'hidden';
        token_input.name = 'g-recaptcha-response';
        token_input.value = token; //トークンを値に設定
        validationForm.appendChild(token_input);
        const action_input = document.createElement('input'); //input 要素を生成
        action_input.type = 'hidden';
        action_input.name = 'action';
        action_input.value = action_name;  //アクション名を値に設定
        validationForm.appendChild(action_input);
        validationForm.submit();  //フォームを送信
      }
    });
  });
});
</script>
</body>
</html>