PHP コンタクトフォーム(お問い合わせページ)の作り方

PHP を使った確認画面のあるコンタクトフォーム(お問い合わせページ)の作成方法についての覚書です。

ユーザが入力した値を保持しながらページ間を移動(遷移)するので、セッションを利用します。

セッションのセキュリティ対策としては、session_regenerate_id() 関数を使ってセッション ID を変更し、CSRF 対策としてセッションの開始時にトークン(ランダムな文字列)を発行し、そのトークンが一致する場合にのみ処理を実行するようにしています(※但し、session_regenerate_id を 呼ぶことでセッションが消失する可能性があります)。

また、JavaScript を使った入力値の検証や自動返信、PHPMailer を使ったメールの送信(Gmail SMTP サーバ)や reCAPTCHA v3 を使ったスパム対策の実装方法についても掲載しています。

【更新 2021年11月17日】 クライアント側の検証を jQuery から JavaScript に変更し、外部からの変数($_POST など)の受け取りを filter_input() を使うように書き換える等一部を変更しました。

【更新 2024年03月25日】 PHP8.1 からは htmlspecialchars() などに null をに渡すと Deprecated エラーになるので、null の場合は空文字列を渡すように変更しました。

関連ページ:

更新日:2024年03月25日

作成日:2020年3月30日

コンタクトフォームの作成

「入力ページ」 → 「確認ページ」 → 「完了ページ」の順で遷移するコンタクトフォームを作成します。

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

以下がそれぞれのページの概要です。

ページ 説明
入力ページ ユーザーが「名前」「Email」「電話番号」「件名」「問い合わせ内容」を入力するページです。必須項目や値のクライアントサイドでの検証は JavaScript で行います。
確認ページ サーバ側で入力された値を PHP で検証して入力された値に問題がなければ入力内容と「送信ボタン」及び「戻るボタン」を表示します。不備がある場合はエラーを表示して再度入力フォームを表示します。
完了ページ 問い合わせの受付が完了したことを知らせるページです。PHP(sendmail)でメールを送信して送信結果を表示します。

フォルダ・ファイル構成

「入力ページ」「確認ページ」「完了ページ」は 「contact」というフォルダ内に配置しています。

また、この例では「libs」というフォルダを作成し、メールの送信情報や必要な関数を記述したファイルを保存し、.htaccesss で外部からアクセスできないようにしています。

必要に応じてファイル名やフォルダ名は適宜変更し、プログラム内の該当箇所も変更します。

├── contact
│   ├── contact.php    //入力ページ
│   ├── confirm.php  //確認ページ
│   └── complete.php //完了ページ
├── libs
│   ├── .htaccess //アクセス制御(このフォルダへの外部からのアクセスを拒否)
│   ├── functions.php //値を検証する関数やエスケープ処理をする関数のファイル
│   └── mailvars.php //メールの送信先などの情報を記述したファイル
└── style.css //スタイルシート

以下は libs フォルダへのアクセスを制御する .htaccesss というファイルの内容です。

.htaccess
deny from all

以下は PHP で入力値を検証する際に使用する関数を記述した functions.php というファイルです。

8行目は、PHP 8.1.x から htmlspecialchars() などの関数に null を渡すと Deprecated エラーになるので、null の場合は空文字列を返すようにしています。

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 というファイルです。

mailvars.php
<?php
//メールの宛先(To)の Email アドレス
define('MAIL_TO', "info@xxxxx.com");
//メールの宛先(To)の名前
define('MAIL_TO_NAME', "宛先の名前 ");
//Cc の Email アドレス
define('MAIL_CC', 'xxxx@xxxxxx.com');
//Cc の名前
define('MAIL_CC_NAME', 'Cc宛先名');
//Bcc の Email アドレス
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-js, .error-php {
  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;
}
/* 確認ページの表 */
.confirm_table {
  margin: 30px 0;
}
.confirm_table table caption{
  caption-side: top;
}
.confirm {
  float: left;
  margin-right: 20px;
}

入力ページ

入力ページはユーザーがコンタクトフォーム(contact.php)にアクセスした際に表示されるページです。

入力ページは PHP、HTML、JavaScript の部分から構成され、それぞれ以下のような内容になります。

  • PHP : セッションの開始や CSRF 対策の固定トークンの生成、確認ページから戻った際の入力値を表示するための値の初期化など
  • HTML :form 要素を使った入力欄や確認ページへ移動するボタン、トークンの値を設定する隠し要素の設定など
  • JavaScript :フォームをサーバへ送信する前の検証(オプション)。
PHP

以下が入力ページ(contact.php)の PHP の部分です。

最初にセッションを使えるように session_start() 関数を呼び出します。

session_start() 関数は必ず Web ブラウザへの出力が行われる前に呼び出す必要があります。

session_start() 関数を呼び出した後は、セッション変数「$_SESSION」が使えるようになりデータを保存することができます。

そして session_start() の直後に session_regenerate_id() 関数を呼び出してセッション ID を変更します(セッションハイジャック対策)。第1パラメータには必ず「TRUE」を指定します

[追記]

session_regenerate_id() のパラメータに TRUE(または true)を指定すると、セッションが消える現象が発生していました(以前は機能していたのですが)。PHP マニュアルにも以下のような記載があります。

警告 現在の session_regenerate_id は、不安定なネットワークをうまく扱えません。 たとえば、モバイルネットワークや WiFi ネットワークです。 よって、 session_regenerate_id を 呼ぶことで、セッションの消失を経験するかもしれません。

13〜19行目は HTML の input 要素の値に出力する変数の初期値の設定です。初回アクセス時は $_SESSION 変数は未定義なので NULL 合体演算子(??)を使って初期値に NULL を設定しています。

22〜27行目は HTML でエラーを表示する span 要素に出力する変数の初期値の設定です。$error は配列になっているので、個々のエラーごとに初期値を設定しています。

30〜33行目では初回アクセス時に CSRF 対策のトークンを random_bytes()bin2hex() を使って生成してセッション変数に代入し、34行目で変数 $ticket にその値を代入しています。

生成されたトークンの値 $ticket は、HTML の隠し属性を指定した input 要素の値に出力され、確認ページに移動する際に検証されるようになっています。

contact.php PHP 部分抜粋
<?php
//セッションを開始
session_start();

//セッションIDを更新して変更(セッションハイジャック対策)
//session_regenerate_id( TRUE ); //TRUE を指定するとセッションが消える!!!
session_regenerate_id();

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

//NULL 合体演算子を使ってセッション変数を初期化(PHP7.0以降)
$name = $_SESSION[ 'name' ] ?? NULL;
$email = $_SESSION[ 'email' ] ?? NULL;
$email_check = $_SESSION[ 'email_check' ] ?? NULL;
$tel = $_SESSION[ 'tel' ] ??  NULL;
$subject = $_SESSION[ 'subject' ] ?? NULL;
$body = $_SESSION[ 'body' ] ?? NULL;
$error = $_SESSION[ 'error' ] ?? NULL;

//個々のエラーを NULL で初期化(PHP7.0以降)
$error_name = $error[ 'name' ] ?? NULL;
$error_email = $error[ 'email' ] ?? NULL;
$error_email_check = $error[ 'email_check' ] ?? NULL;
$error_tel = $error[ 'tel' ] ?? NULL;
$error_subject = $error[ 'subject' ] ?? NULL;
$error_body = $error[ 'body' ] ?? NULL;

//CSRF対策のトークンを生成
if ( !isset( $_SESSION[ 'ticket' ] ) ) {
  //セッション変数にトークンを代入(PHP7.0以降)
  $_SESSION[ 'ticket' ] = bin2hex(random_bytes(32));
}
//トークンを変数に代入(隠しフィールドに挿入する値)
$ticket = $_SESSION[ 'ticket' ];
?>

PHP 7.0 未満の場合

PHP7未満の場合は、NULL 合体演算子(??)は使えないので、isset() を使って初期化します。

上記コードの12〜26行目を isset() を使って記述する場合
//isset() を使った初期化
$name = isset($_SESSION[ 'name' ]) ? $_SESSION[ 'name' ] : NULL;
$email = isset($_SESSION[ 'email' ]) ? $_SESSION[ 'email' ] : NULL;
$email_check = isset($_SESSION[ 'email_check' ]) ? $_SESSION[ 'email_check' ] : NULL;
$tel = isset($_SESSION[ 'tel' ]) ? $_SESSION[ 'tel' ] : NULL;
$subject = isset($_SESSION[ 'subject' ]) ? $_SESSION[ 'subject' ] : NULL;
$body = isset($_SESSION[ 'body' ]) ? $_SESSION[ 'body' ] : NULL;
$error = isset($_SESSION[ 'error' ]) ? $_SESSION[ 'error' ] : NULL;

$error_name = isset($error['name']) ? $error['name'] : NULL;
$error_email = isset($error['email']) ? $error['email'] : NULL;
$error_email_check = isset($error['email_check']) ? $error['email_check'] : NULL;
$error_tel = isset($error['tel']) ? $error['tel'] : NULL;
$error_subject = isset($error['subject']) ? $error['subject'] : NULL;
$error_body = isset($error['body'] ) ? $error['body'] : NULL;

また、PHP7.0 未満ではトークンを生成する際に使用する random_bytes() も使えないので代わりに openssl_random_pseudo_bytes() を使ってトークンを生成します。

上記コードの29〜32行目を openssl_random_pseudo_bytes() を使って記述する場合
//CSRF対策のトークンを生成
if ( !isset( $_SESSION[ 'ticket' ] ) ) {
  //PHP7.0未満(PHP5.3以降)の場合
  $_SESSION[ 'ticket' ] = bin2hex(openssl_random_pseudo_bytes(32));
}
HTML

HTML のフォーム関連の要素を使って入力欄やボタンを表示します。

form 要素の method 属性に post を指定し、action 属性に確認ページ(confirm.php)を指定します。class 属性には validationForm を指定し、JavaScript の検証でフォーム要素を特定するのに使用します。

また、この例の JavaScript の検証では、HTML5 の自動検証は使用しないので form 要素に novalidate 属性を指定してブラウザーによるエラーメッセージの表示を無効にしています。

各コントロールの label 要素には PHP の検証によるエラーを表示する error-php クラスを指定した span 要素を配置し、入力された値は各コントロール要素の value 属性に PHP で出力します。

53行目では input 要素に hidden 属性を指定して非表示にし、CSRF 対策用のトークンの値($ticket)を PHP で出力しています。

contact.php HTML 部分抜粋
<!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>お問い合わせフォーム</h2>
  <p>以下のフォームからお問い合わせください。</p>
  <form id="form" class="validationForm" method="post" action="confirm.php" novalidate>
    <div class="form-group">
      <label for="name">お名前(必須)
        <span class="error-php"><?php 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 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 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 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 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 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>
    <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」-->
    <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>">
    <button name="submitted" type="submit" class="btn btn-primary">確認画面へ</button>
  </form>
</div>
<!-- 検証用 JavaScript の読み込み -->
<script src="formValidation.js"></script>
</body>
</html>

各入力項目は以下のように div 要素で囲まれた label 要素とコントロール要素で構成されています。

PHP の検証で入力内容が条件を満たさないと $error['xxxx'] が設定され、このページに戻されます。

label 要素内には error-php クラスが付与された span 要素があり、確認ページの検証でエラーが設定された場合はこの部分にエラーメッセージを表示します。

h() は値をエスケープ処理する関数で別途 functions.php で定義されています。

input 要素の値(value 属性)には、確認ページから戻って来た場合などでは一度入力した値が変数に保存されているので echo h(変数名) の出力により入力欄にエスケープ処理されて表示されます。

<div class="form-group">
  <label for="name">お名前(必須)
    <span class="error-php"><?php 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>

input 要素の class 属性には JavaScript で検証を行うためのクラス(required や pattern など)を指定しています。form-control と form-group は CSS でスタイルを設定するためのクラスです。

また、data-* 属性(カスタムデータ属性)を使って検証に必要な値を設定したり、独自のエラーメッセージを表示するようにしています(詳細は次項の JavaScript を参照ください)。

JavaScript

このサンプルの場合、JavaScript の検証は別ファイルに記述した検証用スクリプト(formValidation.js)を body の閉じタグの前で読み込んでいますが、script タグに内容を直接記述することもできます。

この検証用スクリプトを使うには HTML で form 要素に validationForm というクラスと、検証対象のフォームコントロールに以下のようなクラスや data-* 属性(カスタムデータ属性)を指定します。

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

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

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

検証は送信時(サーバーに送られる前)に行い、初回送信後にエラーがある場合は input イベントを使って入力された値が変更される度に検証します。

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

以下のスクリプトは、DOMContentLoaded イベントを使って DOM ツリーの構築が完了した時点で実行するようにしています。大まかな内容としてはそれぞれの検証に使用する関数を定義し、フォームの送信時(submit イベント)及び対象の要素の値が変更される際に input や change イベントで検証しています。

検証の結果エラーがあれば、サーバーには送信せずエラーを表示して、最初のエラーの位置までスクロールするようにしています。

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

内容の詳細はコメント付きのコードや、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
//セッションを開始
session_start();
//セッションIDを更新して変更(セッションハイジャック対策)
//session_regenerate_id( TRUE ); //TRUE を指定するとセッションが消える可能性あり!!!
session_regenerate_id();
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';
//NULL 合体演算子を使ってセッション変数を初期化(PHP7以降の場合)
$name = $_SESSION[ 'name' ] ?? NULL;
$email = $_SESSION[ 'email' ] ?? NULL;
$email_check = $_SESSION[ 'email_check' ] ?? NULL;
$tel = $_SESSION[ 'tel' ] ??  NULL;
$subject = $_SESSION[ 'subject' ] ?? NULL;
$body = $_SESSION[ 'body' ] ?? NULL;
$error = $_SESSION[ 'error' ] ?? NULL;

//個々のエラーを NULL で初期化(PHP7以降の場合)
$error_name = $error['name'] ?? NULL;
$error_email = $error['email'] ?? NULL;
$error_email_check = $error['email_check'] ?? NULL;
$error_tel = $error['tel'] ?? NULL;
$error_subject = $error['subject'] ?? NULL;
$error_body = $error['body'] ?? NULL;

//CSRF対策の固定トークンを生成
if ( !isset( $_SESSION[ 'ticket' ] ) ) {
  //セッション変数にトークンを代入(PHP7.0以降)
  $_SESSION[ 'ticket' ] = bin2hex(random_bytes(32));
}
//トークンを変数に代入
$ticket = $_SESSION[ 'ticket' ];
?>
<!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>お問い合わせフォーム</h2>
  <p>以下のフォームからお問い合わせください。</p>
  <form id="form" class="validationForm" method="post" action="confirm.php" novalidate>
    <div class="form-group">
      <label for="name">お名前(必須)
        <span class="error-php"><?php 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 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 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 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 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 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>
    <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」-->
    <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>">
    <button name="submitted" type="submit" class="btn btn-primary">送信</button>
  </form>
</div>
<!-- 検証用 JavaScript の読み込み-->
<script src="formValidation.js"></script>
</body>
</html>

確認ページ

確認ページはユーザーが入力ページでフォームに値を入力後、「確認画面へ」のボタンをクリックして JavaScript の検証及びこのページに記述された PHP の検証を通過した場合に表示されます。

確認ページは PHP、HTML の2つの部分から構成され、それぞれ以下のような内容になります。

  • PHP : 入力ページから POST メソッドで送信されたデータ(入力値)を検証して問題がなければ確認ページを表示し、問題があれば入力ページへ戻す
  • HTML :検証を通過した入力値の表示と、フォームを送信する「送信」ボタン及び入力画面に戻るための「戻る」ボタンを表示
PHP

以下が確認ページ(confirm.php)の PHP の部分です。

セッションを利用するために、session_start() でセッションを開始します。

続いてエスケープ処理やデータチェックを行う関数が記述されているファイル(functions.php)を読み込み、 functions.php で定義されている関数 checkInput() を使って POST されたデータ(配列)をチェックして、もし不正な値が検出されれば die() で処理を中止します(7行目)。

また、入力ページで生成したトークン($_POST[ 'ticket' ])とセッションに保存されているトークン($_SESSION[ 'ticket' ])の値を比較して一致していなければ処理を中止し(13行目)、トークンが存在しない場合も処理を中止します(17行目)。

26〜31行目では filter_input() を使って POST されたデータを取得して、trim() で値の前後の空白文字を削除して変数に代入しています。PHP 8.1.x から trim() に null を渡すと Deprecated エラーになるので、filter_input() の値が null の場合は空文字に変換して trim() を適用しています

filter_input() は指定した名前の変数を外部から受け取り、オプションでそれをフィルタリングする関数ですが、この場合はフィルタは指定せず、値が存在すればその値を、値が存在しなければ NULL を返します。

37〜72行目では入力項目の値を検証し、条件に合致しない場合はエラーとして配列 $error に追加します。

関連項目:PHP フォームの検証(バリデーション)

75〜81行目では、POST されたデータとエラーメッセージ(の配列)をセッションで使用するために $_SESSION 変数に保存しています。

最後に検証結果にエラーがあれば(配列 $error の要素が1つでもあれば)、header() 関数で入力画面にリダイレクトします(入力画面の URL は $_SERVER 変数で組み立てます)。

エラーがなければ、HTML へ移行して入力内容を表示します。

confirm.php PHP 部分抜粋
<?php
//セッションを開始
session_start();
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';
//POSTされたデータをチェック
$_POST = checkInput( $_POST );
//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access Denied!' );
  }
} else {
  //トークンが存在しない場合は処理を中止(直接このページにアクセスするとエラーになる)
  die( 'Access Denied(直接このページにはアクセスできません)' );
}

// 値が 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')) );

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

//値の検証(入力内容が条件を満たさない場合はエラーメッセージを配列 $error に設定)
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 ( $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されたデータとエラーの配列をセッション変数に保存
$_SESSION[ 'name' ] = $name;
$_SESSION[ 'email' ] = $email;
$_SESSION[ 'email_check' ] = $email_check;
$_SESSION[ 'tel' ] = $tel;
$_SESSION[ 'subject' ] = $subject;
$_SESSION[ 'body' ] = $body;
$_SESSION[ 'error' ] = $error;

//チェックの結果にエラーがある場合は入力フォームに戻す
if ( count( $error ) > 0 ) {
  //エラーがある場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
  if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
    $_SERVER[ 'HTTPS' ] = 'on';
  }
  //入力画面(contact.php)の URL
  $url = ( empty( $_SERVER[ 'HTTPS' ] ) ? 'http://' : 'https://' ) . $_SERVER[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  exit;
}
?>

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

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

$url = 'https://'. $_SERVER[ 'SERVER_NAME' ] . $dirname . '/contact.php';
HTML

POST されたデータを前述の PHP で変数に代入した値を使って入力内容を表示しています(17行目〜41行目)。出力する際は functions.php に定義されている h() 関数でエスケープ処理します。

変数 $body(お問い合わせ内容)の出力では、改行を表示するため nl2br() 関数を使用しています。

42〜44行目は入力ページへ戻るためのボタンを表示するフォーム要素で action 属性に contact.php(入力ページ)を指定します。ユーザーが入力した値はすでにセッション変数に入っているため、それらの値を POST する必要はありません。

45〜53行目は「送信ボタン」を表示するフォーム要素で、action 属性に complete.php(完了ページ)を指定します。こちらのフォームでは CSRF対策のトークンの値($ticket)を POST する必要があります。

confirm.php HTML 部分抜粋
<!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>
</head>
<body>
<div class="container">
  <h2>お問い合わせ確認画面</h2>
  <p>以下の内容でよろしければ「送信する」をクリックしてください。<br>
    内容を変更する場合は「戻る」をクリックして入力画面にお戻りください。</p>
  <div class="table-responsive">
    <table class="table table-bordered">
      <caption>ご入力内容</caption>
      <tr>
        <th>お名前</th>
        <td><?php echo h($name); ?></td>
      </tr>
      <tr>
        <th>Email</th>
        <td><?php echo h($email); ?></td>
      </tr>
      <tr>
        <th>お電話番号</th>
        <td><?php echo h($tel); ?></td>
      </tr>
      <tr>
        <th>件名</th>
        <td><?php echo h($subject); ?></td>
      </tr>
      <tr>
        <th>お問い合わせ内容</th>
        <td><?php echo nl2br(h($body)); ?></td>
      </tr>
    </table>
  </div>
  <form action="contact.php" method="post" class="confirm">
    <button type="submit" class="btn btn-secondary">戻る</button>
  </form>
  <form action="complete.php" method="post" class="confirm">
    <!-- 完了ページへ渡すトークンの隠しフィールド -->
    <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>">
    <button type="submit" class="btn btn-success">送信する</button>
  </form>
</div>
</body>
</html>
<?php
//セッションを開始
session_start();
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';
//POSTされたデータをチェック
$_POST = checkInput( $_POST );
//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access Denied!' );
  }
} else {
  //トークンが存在しない場合は処理を中止(直接このページにアクセスするとエラーになる)
  die( 'Access Denied(直接このページにはアクセスできません)' );
}
// 値が 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')) );
//エラーメッセージを保存する配列の初期化
$error = array();
//値の検証(入力内容が条件を満たさない場合はエラーメッセージを配列 $error に設定)
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 ( $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されたデータとエラーの配列をセッション変数に保存
$_SESSION[ 'name' ] = $name;
$_SESSION[ 'email' ] = $email;
$_SESSION[ 'email_check' ] = $email_check;
$_SESSION[ 'tel' ] = $tel;
$_SESSION[ 'subject' ] = $subject;
$_SESSION[ 'body' ] = $body;
$_SESSION[ 'error' ] = $error;
//チェックの結果にエラーがある場合は入力フォームに戻す
if ( count( $error ) > 0 ) {
  //エラーがある場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用(オプション)
  if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
    $_SERVER[ 'HTTPS' ] = 'on';
  }
  //入力画面(contact.php)の URL
  $url = ( empty( $_SERVER[ 'HTTPS' ] ) ? 'http://' : 'https://' ) . $_SERVER[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  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>
</head>
<body>
<div class="container">
  <h2>お問い合わせ確認画面</h2>
  <p>以下の内容でよろしければ「送信する」をクリックしてください。<br>
    内容を変更する場合は「戻る」をクリックして入力画面にお戻りください。</p>
  <div class="table-responsive confirm_table">
    <table class="table table-bordered">
      <caption>ご入力内容</caption>
      <tr>
        <th>お名前</th>
        <td><?php echo h($name); ?></td>
      </tr>
      <tr>
        <th>Email</th>
        <td><?php echo h($email); ?></td>
      </tr>
      <tr>
        <th>お電話番号</th>
        <td><?php echo h($tel); ?></td>
      </tr>
      <tr>
        <th>件名</th>
        <td><?php echo h($subject); ?></td>
      </tr>
      <tr>
        <th>お問い合わせ内容</th>
        <td><?php echo nl2br(h($body)); ?></td>
      </tr>
    </table>
  </div>
  <form action="contact.php" method="post" class="confirm">
    <button type="submit" class="btn btn-secondary">戻る</button>
  </form>
  <form action="complete.php" method="post" class="confirm">
    <!-- 完了ページへ渡すトークンの隠しフィールド -->
    <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>">
    <button type="submit" class="btn btn-success">送信する</button>
  </form>
</div>
</body>
</html>

完了ページ

ユーザーが確認ページで、「送信する」のボタンをクリックすると form 要素の action 属性に指定されている complete.php に method 属性で指定されている post メソッドで送信されます。

完了ページでは、CSRF 対策の固定トークンの検証をして問題がなければユーザの入力した値を使ってメール本文を作成してメール送信の処理を行います。

PHP

以下が確認ページ(complete.php)の PHP の部分です。

セッションを利用するために、session_start() でセッションを開始し、続いてエスケープ処理やデータチェックを行う関数が記述されているファイル(functions.php)とメールアドレスなどを記述してある mailvars.php を読み込みます。

10行目はメール本文にお問い合わせ日時を日本時間で記載するためにタイムゾーンを設定しています。

そして functions.php で定義されている関数 checkInput() を使って POST されたデータを検証して、もし不正な値が検出されれば die() で処理を中止します(13行目)。

16〜22行目では確認ページの HTML で input 要素に指定した値($ticket)とセッションに保存されているトークン($_SESSION[ 'ticket' ])の値を比較して一致していなければ処理を中止します。

トークンが存在しない場合も処理を中断しても良いのですが、完了ページでページを再読み込みした場合を考慮して入力ページにリダイレクトしています。

但し、header() 関数実行後は exit を記述しないと、以降の処理が実行されてしまうため再読み込みの際に値のないメールが送信されてしまいます(23〜34行目)。

38〜50行目は入力された値をエスケープ処理してそれらをもとにメール本文を作成しています。

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

mb_send_mail() はメールの送信が成功した場合は true を、失敗した場合は false を戻り値として返すので $result に代入しておきます。

メールの送信が成功した場合はすべてのセッション変数を消去し、session_destroy() でセッションを破棄します(77〜80行目)。

HTML ではメールの送信結果($result)をもとにメッセージ(成功または失敗)を表示します。

complete.php PHP 部分抜粋
<?php
//セッションを開始
session_start();
//エスケープ処理やデータをチェックする関数を記述したファイルの読み込み
require '../libs/functions.php';
//メールアドレス等を記述したファイルの読み込み
require '../libs/mailvars.php';

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

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

//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access denied' );
  }
} else {
  //トークンが存在しない場合(入力ページにリダイレクト)
  //die( 'Access Denied(直接このページにはアクセスできません)' ); //処理を中止する場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  //サーバー変数 $_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[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  exit; //忘れないように
}

//変数にエスケープ処理したセッション変数の値を代入
$name = h( $_SESSION[ 'name' ] );
$email = h( $_SESSION[ 'email' ] ) ;
$tel =  h( $_SESSION[ 'tel' ] ) ;
$subject = h( $_SESSION[ 'subject' ] );
$body = h( $_SESSION[ 'body' ] );

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

//-------- sendmail(mb_send_mail)を使ったメールの送信処理------------

//メールの宛先(名前<メールアドレス> の形式)。値は 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 ) {
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去
  session_destroy(); //セッションを破棄
} else {
  //送信失敗時(もしあれば)
}
?>
HTML

メールの送信処理の結果をユーザに表示します。

メールの送信処理が完了すると $result に結果の真偽値が入っているので、その値で表示する内容を切り替えています。

送信が成功した場合は15〜17行目が表示され、失敗した場合は19〜21行目が表示されます。

<!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>お問い合わせフォーム</h2>
  <?php if ( $result ): ?>
  <h3>送信完了!</h3>
  <p>お問い合わせいただきありがとうございます。</p>
  <p>送信完了いたしました。</p>
  <?php else: ?>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>ご迷惑をおかけして誠に申し訳ございません。</p>
  <?php endif; ?>
</div>
</body>
</html>
<?php
//セッションを開始
session_start();
//エスケープ処理やデータをチェックする関数を記述したファイルの読み込み
require '../libs/functions.php';
//メールアドレス等を記述したファイルの読み込み
require '../libs/mailvars.php';

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

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

//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access denied' );
  }
} else {
  //トークンが存在しない場合(入力ページにリダイレクト)
  //die( 'Access Denied(直接このページにはアクセスできません)' );  //処理を中止する場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  //サーバー変数 $_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[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  exit; //忘れないように
}

//変数にエスケープ処理したセッション変数の値を代入
$name = h( $_SESSION[ 'name' ] );
$email = h( $_SESSION[ 'email' ] ) ;
$tel =  h( $_SESSION[ 'tel' ] ) ;
$subject = h( $_SESSION[ 'subject' ] );
$body = h( $_SESSION[ 'body' ] );

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

//-------- sendmail(mb_send_mail)を使ったメールの送信処理------------

//メールの宛先(名前<メールアドレス> の形式)。値は 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 ) {
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去
  session_destroy(); //セッションを破棄
} else {
  //送信失敗時(もしあれば)
}
?>
<!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>お問い合わせフォーム</h2>
  <?php if ( $result ): ?>
  <h3>送信完了!</h3>
  <p>お問い合わせいただきありがとうございます。</p>
  <p>送信完了いたしました。</p>
  <?php else: ?>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>ご迷惑をおかけして誠に申し訳ございません。</p>
  <?php endif; ?>
</div>
</body>
</html>

reCAPTCHA v3 を使う

前述のコンタクトフォームに reCAPTCHA v3 を実装する例です。

以下のサンプルでは確認ページの画面右下に reCAPTCHA v3 のウィジェットが表示されます。また、完了ページに確認用にスコアなどの判定結果を表示します。

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

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

以下がサンプルのフォルダ構成です。reCAPTCHA のサイトキーとシークレットキーを記述してあるファイル(9行目)を追加しています。

├── contact
│   ├── contact.php    //入力ページ
│   ├── confirm.php  //確認ページ
│   └── complete.php //完了ページ
├── libs
│   ├── .htaccess //アクセス制御(このフォルダへの外部からのアクセスを拒否)
│   ├── functions.php //値を検証する関数やエスケープ処理をする関数のファイル
│   ├── mailvars.php //メールの送信先などの情報を記述したファイル
│   └── recaptchavars.php //reCAPTCHA のサイトキーとシークレットキー(追加)
└── style.css //スタイルシート

入力ページ

この例では reCAPTCHA v3 は確認ページに実装して「送信」ボタンをクリックした際に reCAPTCHA のトークンを生成して送信します。

そのため、入力ページは前述のコンタクトフォームの入力ページ(contact.php)と同じです。

入力ページに reCAPTCHA のウィジェットを表示

入力ページに reCAPTCHA のウィジェットを表示するには、ウィジェットを表示する API にサイトキーを指定して読み込みます。

HTML での API の読み込み
<script src="https://www.google.com/recaptcha/api.js?render=サイトキー"></script> 

PHP では以下のコメントアウトを外して、別ファイルに記述してあるサイトキーの情報を読み込みます。

PHP での reCAPTCHA サイトキーの読み込み
//require '../libs/recaptchavars.php'; //reCAPTCHA サイトキーを記述したファイルの読み込み
//$siteKey = V3_SITEKEY; // reCAPTCHA サイトキー 
<?php
//セッションを開始
session_start();
//セッションIDを更新して変更(セッションハイジャック対策)
//session_regenerate_id( TRUE ); //TRUE を指定するとセッションが消える可能性あり!!!
session_regenerate_id();
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';
//reCAPTCHA ウィジェットを表示する場合は以下の2行と API 読み込み(94行目)のコメントアウトを外す
//require '../libs/recaptchavars.php'; //reCAPTCHA サイトキーを記述したファイルの読み込み
//$siteKey = V3_SITEKEY; // reCAPTCHA サイトキー

//NULL 合体演算子を使ってセッション変数を初期化(PHP7以降の場合)
$name = $_SESSION[ 'name' ] ?? NULL;
$email = $_SESSION[ 'email' ] ?? NULL;
$email_check = $_SESSION[ 'email_check' ] ?? NULL;
$tel = $_SESSION[ 'tel' ] ??  NULL;
$subject = $_SESSION[ 'subject' ] ?? NULL;
$body = $_SESSION[ 'body' ] ?? NULL;
$error = $_SESSION[ 'error' ] ?? NULL;

//個々のエラーを NULL で初期化(PHP7以降の場合)
$error_name = $error['name'] ?? NULL;
$error_email = $error['email'] ?? NULL;
$error_email_check = $error['email_check'] ?? NULL;
$error_tel = $error['tel'] ?? NULL;
$error_subject = $error['subject'] ?? NULL;
$error_body = $error['body'] ?? NULL;

//CSRF対策の固定トークンを生成
if ( !isset( $_SESSION[ 'ticket' ] ) ) {
  //セッション変数にトークンを代入
  $_SESSION[ 'ticket' ] = bin2hex( random_bytes(32) );
}
//トークンを変数に代入
$ticket = $_SESSION[ 'ticket' ];
?>
<!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>お問い合わせフォーム</h2>
  <p>以下のフォームからお問い合わせください。</p>
  <form id="form" class="validationForm" method="post" action="confirm.php" novalidate>
    <div class="form-group">
      <label for="name">お名前(必須)
        <span class="error-php"><?php 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 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 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 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 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 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>
    <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」-->
    <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>">
    <button name="submitted" type="submit" class="btn btn-primary">確認画面へ</button>
  </form>
</div>
<!-- reCAPTCHA ウィジェットを表示する場合は以下のコメントアウトを外す -->
<!--<script src="https://www.google.com/recaptcha/api.js?render=<?php echo $siteKey; ?>"></script>-->
<script src="formValidation.js"></script>
</body>
</html>

確認ページ

確認ページでは「送信」ボタンをクリックした際に reCAPTCHA のトークンを生成するようにします。

reCAPTCHA を使用するためのサイトキーとシークレットキーは mailvars.php などのファイルを保存してある外部からアクセスできないフォルダ libs に recaptchavars.php という名前で保存します(または mailvars.php に他の情報と共に記述するなど安全な場所に保存します)。

recaptchavars.php
<?php
// reCAPTCHA v3 サイトキー
define('V3_SITEKEY', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxx');
// reCAPTCHA v3 シークレットキー
define('V3_SECRETKEY', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxx');
PHP

確認ページ(confirm.php)の PHP ではサイトキーとシークレットキーを記述したファイル(recaptchavars.php)を読み込み、サイトキーを変数($siteKey)に代入します。

変数 $siteKey は reCAPTCHA の API の読み込み及びトークンの取得で使用します。

confirm.php PHP 一部抜粋
<?php
//セッションを開始
session_start();
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';

//reCAPTCHA サイトキーを記述したファイルの読み込み(★追加)
require '../libs/recaptchavars.php';
// reCAPTCHA サイトキー(★追加)
$siteKey = V3_SITEKEY;

・・・以下省略(以降は前述のコンタクトフォームの confirm.php と全く同じ)
<?php
//セッションを開始
session_start();
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';

//reCAPTCHA サイトキーを記述したファイルの読み込み(★追加)
require '../libs/recaptchavars.php';
// reCAPTCHA サイトキー(★追加)
$siteKey = V3_SITEKEY;

//POSTされたデータをチェック
$_POST = checkInput( $_POST );
//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access Denied!' );
  }
} else {
  //トークンが存在しない場合は処理を中止(直接このページにアクセスするとエラーになる)
  die( 'Access Denied(直接このページにはアクセスできません)' );
}
// 値が 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')) );

//エラーメッセージを保存する配列の初期化
$error = array();
//値の検証(入力内容が条件を満たさない場合はエラーメッセージを配列 $error に設定)
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 ( $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されたデータとエラーの配列をセッション変数に保存
$_SESSION[ 'name' ] = $name;
$_SESSION[ 'email' ] = $email;
$_SESSION[ 'email_check' ] = $email_check;
$_SESSION[ 'tel' ] = $tel;
$_SESSION[ 'subject' ] = $subject;
$_SESSION[ 'body' ] = $body;
$_SESSION[ 'error' ] = $error;
//チェックの結果にエラーがある場合は入力フォームに戻す
if ( count( $error ) > 0 ) {
  //エラーがある場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
  if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
    $_SERVER[ 'HTTPS' ] = 'on';
  }
  //入力画面(contact.php)の URL
  $url = ( empty( $_SERVER[ 'HTTPS' ] ) ? 'http://' : 'https://' ) . $_SERVER[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  exit;
}
?>
HTML

JavaScript で送信するフォームを指定しやすいように「送信」ボタンを記述してある form 要素に id 属性(#complete)を指定しています(24行目)。

また、render パラメータにサイトキーを指定(変数 $siteKey に代入してあるので PHP で出力)して reCAPTCHA の API を読み込みます(31行目)。

confirm.php HTML 一部抜粋
<body>
<div class="container">
  <h2>お問い合わせ確認画面</h2>
  <p>以下の内容でよろしければ「送信する」をクリックしてください。<br>
    内容を変更する場合は「戻る」をクリックして入力画面にお戻りください。</p>
  <div class="table-responsive">
    <table class="table table-bordered">
      <caption>ご入力内容</caption>
      <tr>
        <th>お名前</th>
        <td><?php echo h($name); ?></td>
      </tr>
      ・・・中略・・・
      <tr>
        <th>お問い合わせ内容</th>
        <td><?php echo nl2br(h($body)); ?></td>
      </tr>
    </table>
  </div>
  <form action="contact.php" method="post" class="confirm">
    <button type="submit" class="btn btn-secondary">戻る</button>
  </form>
  <!-- form 要素に id="complete" を指定 -->
  <form id="complete" action="complete.php" method="post" class="confirm">
    <!-- 完了ページへ渡すトークンの隠しフィールド -->
    <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>">
    <button type="submit" class="btn btn-success">送信する</button>
  </form>
</div>
<!-- reCAPTCHA v3 の読み込み(追加) サイトキーは PHP で出力-->
<script src="https://www.google.com/recaptcha/api.js?render=<?php echo $siteKey; ?>"></script>
<script> //reCAPTCHA のトークンを生成する処理(次項参照) </script>
<!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>
</head>
<body>
<div class="container">
  <h2>お問い合わせ確認画面</h2>
  <p>以下の内容でよろしければ「送信する」をクリックしてください。<br>
    内容を変更する場合は「戻る」をクリックして入力画面にお戻りください。</p>
  <div class="table-responsive">
    <table class="table table-bordered">
      <caption>ご入力内容</caption>
      <tr>
        <th>お名前</th>
        <td><?php echo h($name); ?></td>
      </tr>
      <tr>
        <th>Email</th>
        <td><?php echo h($email); ?></td>
      </tr>
      <tr>
        <th>お電話番号</th>
        <td><?php echo h($tel); ?></td>
      </tr>
      <tr>
        <th>件名</th>
        <td><?php echo h($subject); ?></td>
      </tr>
      <tr>
        <th>お問い合わせ内容</th>
        <td><?php echo nl2br(h($body)); ?></td>
      </tr>
    </table>
  </div>
  <form action="contact.php" method="post" class="confirm">
    <button type="submit" class="btn btn-secondary">戻る</button>
  </form>
  <!-- ★ id="complete" を追加 -->
  <form id="complete" action="complete.php" method="post" class="confirm">
    <!-- 完了ページへ渡すトークンの隠しフィールド -->
    <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>">
    <button type="submit" class="btn btn-success">送信する</button>
  </form>
</div>
<!-- reCAPTCHA v3 の読み込み(★追加) サイトキーは PHP で出力-->
<script src="https://www.google.com/recaptcha/api.js?render=<?php echo $siteKey; ?>"></script>
<script> //reCAPTCHA のトークンを生成する処理(次項参照) </script>
</body>
</html>
JavaScript

JavaScript を使って「送信」ボタンをクリックした際に reCAPTCHA のトークンを生成(取得)します。以下は body の閉じタグの直前(reCAPTCHA v3 の読み込みの後)に記述します。

id 属性に complete を指定した form 要素を取得し、addEventListener() で登録した submit イベントハンドラでトークンを取得して complete.php へ送信します。

submit イベントハンドラでは preventDefault() で送信を一時中断し、hidden 属性を指定した2つの input 要素を作成して、パラメータのトークン(token)とアクション名(この例では contact)を要素の値(value)に設定し、フォームに追加して送信します。

関連ページ(項目):reCAPTCHA v3 クライアント側の実装

<script>
//id 属性に complete を指定した form 要素を取得
const myForm = document.getElementById('complete');
//上記で取得したフォーム要素に submit イベントハンドラを設定
myForm.addEventListener('submit', (e) => {
  //デフォルトの動作(送信)を停止
  e.preventDefault();
  const action_name = 'contact'; //アクション名
  //トークンを取得
  grecaptcha.ready(function() {
    grecaptcha.execute('<?php echo $siteKey; ?>', {action: action_name}).then(function(token) {
      const token_input = document.createElement('input'); //input 要素を生成
      token_input.type = 'hidden';
      token_input.name = 'g-recaptcha-response';
      token_input.value = token; //トークンを値に設定
      myForm.appendChild(token_input);  //フォームに input 要素を追加
      const action_input = document.createElement('input'); //input 要素を生成
      action_input.type = 'hidden';
      action_input.name = 'action';
      action_input.value = action_name;  //アクション名を値に設定
      myForm.appendChild(action_input);  //フォームに input 要素を追加
      myForm.submit();  //フォームを送信
    });
  });
});
</script>
<?php
//セッションを開始
session_start();
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';

//reCAPTCHA サイトキーを記述したファイルの読み込み(★追加)
require '../libs/recaptchavars.php';
// reCAPTCHA サイトキー(★追加)
$siteKey = V3_SITEKEY;

//POSTされたデータをチェック
$_POST = checkInput( $_POST );
//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access Denied!' );
  }
} else {
  //トークンが存在しない場合は処理を中止(直接このページにアクセスするとエラーになる)
  die( 'Access Denied(直接このページにはアクセスできません)' );
}
//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') );

//エラーメッセージを保存する配列の初期化
$error = array();
//値の検証(入力内容が条件を満たさない場合はエラーメッセージを配列 $error に設定)
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 ( $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されたデータとエラーの配列をセッション変数に保存
$_SESSION[ 'name' ] = $name;
$_SESSION[ 'email' ] = $email;
$_SESSION[ 'email_check' ] = $email_check;
$_SESSION[ 'tel' ] = $tel;
$_SESSION[ 'subject' ] = $subject;
$_SESSION[ 'body' ] = $body;
$_SESSION[ 'error' ] = $error;
//チェックの結果にエラーがある場合は入力フォームに戻す
if ( count( $error ) > 0 ) {
  //エラーがある場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
  if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
    $_SERVER[ 'HTTPS' ] = 'on';
  }
  //入力画面(contact.php)の URL
  $url = ( empty( $_SERVER[ 'HTTPS' ] ) ? 'http://' : 'https://' ) . $_SERVER[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  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>
</head>
<body>
<div class="container">
  <h2>お問い合わせ確認画面</h2>
  <p>以下の内容でよろしければ「送信する」をクリックしてください。<br>
    内容を変更する場合は「戻る」をクリックして入力画面にお戻りください。</p>
  <div class="table-responsive confirm_table">
    <table class="table table-bordered" style="max-width:600px;">
      <caption>ご入力内容</caption>
      <tr>
        <th>お名前</th>
        <td><?php echo h($name); ?></td>
      </tr>
      <tr>
        <th>Email</th>
        <td><?php echo h($email); ?></td>
      </tr>
      <tr>
        <th>お電話番号</th>
        <td><?php echo h($tel); ?></td>
      </tr>
      <tr>
        <th>件名</th>
        <td><?php echo h($subject); ?></td>
      </tr>
      <tr>
        <th style="white-space: nowrap;">お問い合わせ内容</th>
        <td><?php echo nl2br(h($body)); ?></td>
      </tr>
    </table>
  </div>
  <form action="contact.php" method="post" class="confirm">
    <button type="submit" class="btn btn-secondary">戻る</button>
  </form>
  <form id="complete" action="complete.php" method="post" class="confirm"><!-- ★ id="complete" 追加 -->
    <!-- 完了ページへ渡すトークンの隠しフィールド -->
    <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>">
    <button type="submit" class="btn btn-success">送信する</button>
  </form>
  <p style="clear: both;">※サンプルですので実際にはメールは送信されません</p>
</div>
<!-- reCAPTCHA v3 の読み込み(★追加) -->
<script src="https://www.google.com/recaptcha/api.js?render=<?php echo $siteKey; ?>"></script>
<script>
//reCAPTCHA v3 トークン取得(★追加)
//id 属性に complete を指定した form 要素を取得
const myForm = document.getElementById('complete');
//上記で取得したフォーム要素に submit イベントハンドラを設定
myForm.addEventListener('submit', (e) => {
  //デフォルトの動作(送信)を停止
  e.preventDefault();
  const action_name = 'contact'; //アクション名
  //トークンを取得
  grecaptcha.ready(function() {
    grecaptcha.execute('<?php echo $siteKey; ?>', {action: action_name}).then(function(token) {
      const token_input = document.createElement('input'); //input 要素を生成
      token_input.type = 'hidden';
      token_input.name = 'g-recaptcha-response';
      token_input.value = token; //トークンを値に設定
      myForm.appendChild(token_input);  //フォームに input 要素を追加
      const action_input = document.createElement('input'); //input 要素を生成
      action_input.type = 'hidden';
      action_input.name = 'action';
      action_input.value = action_name;  //アクション名を値に設定
      myForm.appendChild(action_input);  //フォームに input 要素を追加
      myForm.submit();  //フォームを送信
    });
  });
});
</script>
</body>
</html>

完了ページ

完了ページでは、確認ページから送信された reCAPTCHA のトークンとアクション名を reCAPTCHA の API を使って検証します。

そして検証結果に問題がなければメールの送信処理を実行します。

PHP

サイトキーとシークレットキーを変数($siteKey と $secretKey)に代入しておきます。

確認ページから送信された reCAPTCHA のトークンとアクション名を変数に代入し、reCAPTCHA の検証結果を格納する変数($rcv3_result)に初期値を設定します(41〜46行目)。

reCAPTCHA のトークンとアクション名が取得できていればそれらの値を使って API からレスポンスを取得して判定し、判定結果を変数($rcv3_result)に代入します(49〜81行目)。

以下の例では判定の基準としてスコアの値が0.5以上であれば合格としていますが、この値を変更することで判定を調整することができます(例えば、0.95以上とすれば、殆どは失敗します)。

そして判定結果がOK(合格)であれば、メールを送信します。

関連ページ(項目):reCAPTCHA v3 PHP を使った検証

<?php
//セッションを開始
session_start();
//エスケープ処理やデータをチェックする関数を記述したファイルの読み込み
require '../libs/functions.php';
//メールアドレス等を記述したファイルの読み込み
require '../libs/mailvars.php';

//reCAPTCHA サイトキーを記述したファイルの読み込み(★追加)
require '../libs/recaptchavars.php';
// reCAPTCHA サイトキー(★追加)
$siteKey = V3_SITEKEY;
// reCAPTCHA シークレットキー(★追加)
$secretKey = V3_SECRETKEY;

//POSTされたデータをチェック
$_POST = checkInput( $_POST );
//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access denied' );
  }
} else {
  //トークンが存在しない場合(入力ページ contact.php にリダイレクト)
  //die( 'Access Denied(直接このページにはアクセスできません)' );  //処理を中止する場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
  if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
    $_SERVER[ 'HTTPS' ] = 'on';
  }
  //入力画面(contact.php)の URL
  $url = ( empty( $_SERVER[ 'HTTPS' ] ) ? 'http://' : 'https://' ) . $_SERVER[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  exit; //忘れないように
}

//reCAPTCHA トークン(★追加)
$token = filter_input(INPUT_POST, 'g-recaptcha-response');
//reCAPTCHA アクション名 (★追加)
$action = filter_input(INPUT_POST, 'action');
//reCAPTCHA の検証を通過したかどうかの真偽値(★追加)
$rcv3_result = false;

// reCAPTCHA のトークンとアクション名が取得できていれば(★追加)
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 以上の場合は合格
    $rcv3_result = true;
  } else {
    // 上記以外の場合は 不合格
    $rcv3_result = false;
  }
}

//メールの送信結果の初期値を false に
$result = false;

//reCAPTCHA の検証結果が合格の場合はメール送信処理を実行
if ( $rcv3_result ) { //(★追加)

/*・・・メールの送信処理の記述(省略)・・・*/

}

//メール送信の結果で分岐
if ( $result ) {
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去
  session_destroy(); //セッションを破棄
} else {
  //送信失敗時(もしあれば)
}
?>
HTML

HTML では送信結果(成功または失敗)により表示を切り替えていて、前述のコンタクトフォームの場合と同じです。

以下のような reCAPTCHA の判定結果の確認(テスト)用の表示を記述していますが、実際のフォームでは記述しません。

<?php if (isset($rc_result )): ?>
<h4>reCAPTCHA 判定結果表示</h4>
<ul>
  <li><?php echo 'success 判定 :' . $rc_result->success; ?></li>
  <li><?php echo 'アクション名 : ' . $rc_result->action ?></li>
  <li><?php echo 'スコア : ' . $rc_result->score; ?></li>
</ul>
<h4>reCAPTCHA API レスポンス</h4>
<pre><?php var_dump($rc_result ); ?></pre>
<?php endif; ?>

サンプルでは上記9行目の var_dump($rc_result ) の部分はコメントアウトして出力しないようにしていますが、出力すると以下のような内容が表示されます。

object(stdClass)#1 (5) {
  ["success"]=>
  bool(true)
  ["challenge_ts"]=>
  string(20) "2021-11-16T00:39:25Z"
  ["hostname"]=>
  string(23) "www.webdesignleaves.com"
  ["score"]=>
  float(0.9)
  ["action"]=>
  string(7) "contact"
}
<?php
//セッションを開始
session_start();
//エスケープ処理やデータをチェックする関数を記述したファイルの読み込み
require '../libs/functions.php';
//メールアドレス等を記述したファイルの読み込み
require '../libs/mailvars.php';

//reCAPTCHA サイトキーを記述したファイルの読み込み(★追加)
require '../libs/recaptchavars.php';
// reCAPTCHA サイトキー(★追加)
$siteKey = V3_SITEKEY;
// reCAPTCHA シークレットキー(★追加)
$secretKey = V3_SECRETKEY;

//POSTされたデータをチェック
$_POST = checkInput( $_POST );
//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access denied' );
  }
} else {
  //トークンが存在しない場合(入力ページ contact.php にリダイレクト)
  //die( 'Access Denied(直接このページにはアクセスできません)' );  //処理を中止する場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
  if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
    $_SERVER[ 'HTTPS' ] = 'on';
  }
  //入力画面(contact.php)の URL
  $url = ( empty( $_SERVER[ 'HTTPS' ] ) ? 'http://' : 'https://' ) . $_SERVER[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  exit; //忘れないように
}

//reCAPTCHA トークン(★追加)
$token = filter_input(INPUT_POST, 'g-recaptcha-response');
//reCAPTCHA アクション名 (★追加)
$action = filter_input(INPUT_POST, 'action');
//reCAPTCHA の検証を通過したかどうかの真偽値(★追加)
$rcv3_result = false;

// reCAPTCHA のトークンとアクション名が取得できていれば(★追加)
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 以上の場合は合格
    $rcv3_result = true;
  } else {
    // 上記以外の場合は 不合格
    $rcv3_result = false;
  }
}

//メールの送信結果の初期値を false に
$result = false;

//reCAPTCHA の検証結果が合格の場合はメール送信処理を実行
if ( $rcv3_result ) { //(★追加)

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

  //変数にエスケープ処理したセッション変数の値を代入
  $name = h( $_SESSION[ 'name' ] );
  $email = h( $_SESSION[ 'email' ] );
  $tel = h( $_SESSION[ 'tel' ] );
  $subject = h( $_SESSION[ 'subject' ] );
  $body = h( $_SESSION[ 'body' ] );

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

  //-------- sendmail(mb_send_mail)を使ったメールの送信処理------------

  //メールの宛先(名前<メールアドレス> の形式)。値は 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 ) {
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去
  session_destroy(); //セッションを破棄
} else {
  //送信失敗時(もしあれば)
}
?>
<!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>お問い合わせフォーム</h2>
  <?php if ( $result ): ?>
  <h3>送信完了!</h3>
  <p>お問い合わせいただきありがとうございます。</p>
  <p class="success">送信完了いたしました。</p>
  <p style="font-size: 18px;">★ サンプルですので実際にはメールは送信されません。★</p>
  <?php else: ?>
  <p class="fail">申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>ご迷惑をおかけして誠に申し訳ございません。</p>
  <?php endif; ?>

  <!-- ここから reCAPTCHA 結果表示(テスト用)-->
  <?php if (isset($rc_result )): ?>
  <h4>reCAPTCHA 判定結果表示(テスト用)</h4>
  <ul>
    <li><?php echo 'success 判定 :' . $rc_result->success; ?></li>
    <li><?php echo 'アクション名 : ' . $rc_result->action ?></li>
    <li><?php echo 'スコア : ' . $rc_result->score; ?></li>
  </ul>
  <h4>reCAPTCHA API レスポンス</h4>
  <pre><?php var_dump($rc_result ); ?></pre>
  <?php endif; ?>
  <!-- ここまで reCAPTCHA 結果表示(テスト用)-->
</div>
</body>
</html>

自動返信

最初のサンプルを変更して、ユーザがコンタクトフォームを送信して成功したらユーザに確認のメールを自動的に返信する例です。

完了ページのメールの送信処理の部分に自動返信の記述を追加します。入力ページや確認ページに変更はありません。

18〜23行目でメールの送信処理を行って、メールの送信が成功すると $result は true になるので、その場合は26行目以降が実行されます。

自動返信も mb_send_mail() を使って送信します(51〜55行目)。

また、$show_autoresponse_msg という変数を使って自動返信の結果を画面に表示するかどうかを切り替えられるようにしています。

complete.php メール送信部分抜粋
//-------- sendmail(mb_send_mail)を使ったメールの送信処理------------

//メールの宛先(名前<メールアドレス> の形式)。値は 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 ) {
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去
  session_destroy(); //セッションを破棄

  //自動返信メールの送信処理
  //自動返信メールの送信が成功したかどうかのメッセージを表示する場合は true
  $show_autoresponse_msg = true;
  //ヘッダー情報
  $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;

  //自動返信の送信(結果を変数 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 );
  }
} else {
  //送信失敗時(もしあれば)
}

以下は送信結果を表示する部分の HTML です。

8〜14行目が自動返信メールの送信結果の表示部分です。この表示が不要な場合は上記 PHP の 33行目の $show_autoresponse_msg を false に設定します。

complete.php 一部抜粋
<body>
<div class="container">
  <h2>お問い合わせフォーム</h2>
  <?php if ( $result ): ?>
  <h3>送信完了!</h3>
  <p>お問い合わせいただきありがとうございます。</p>
  <p>送信完了いたしました。</p>
    <?php if ( $show_autoresponse_msg ): ?>
      <?php if ( $result2 ): ?>
      <p>確認の自動返信メールを <?php echo $email; ?> へお送りいたしました。</p>
      <?php else: ?>
      <p>確認の自動返信メールを送信できませんでした。</p>
      <?php endif; ?>
    <?php endif; ?>
  <?php else: ?>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>ご迷惑をおかけして誠に申し訳ございません。</p>
  <?php endif; ?>
</div>
</body>
<?php
//セッションを開始
session_start();
//エスケープ処理やデータをチェックする関数を記述したファイルの読み込み
require '../libs/functions.php';
//メールアドレス等を記述したファイルの読み込み
require '../libs/mailvars.php';

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

//POSTされたデータをチェック
$_POST = checkInput( $_POST );
//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access denied' );
  }
} else {
  //トークンが存在しない場合(入力ページにリダイレクト)
  //die( 'Access Denied(直接このページにはアクセスできません)' );  //処理を中止する場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  //サーバー変数 $_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[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  exit; //忘れないように
}

//変数にエスケープ処理したセッション変数の値を代入
$name = h( $_SESSION[ 'name' ] );
$email = h( $_SESSION[ 'email' ] ) ;
$tel =  h( $_SESSION[ 'tel' ] ) ;
$subject = h( $_SESSION[ 'subject' ] );
$body = h( $_SESSION[ 'body' ] );

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

//-------- sendmail(mb_send_mail)を使ったメールの送信処理------------

//メールの宛先(名前<メールアドレス> の形式)。値は 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 ) {
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去
  session_destroy(); //セッションを破棄

  //自動返信メールの送信処理
  //自動返信メールの送信が成功したかどうかのメッセージを表示する場合は true
  $show_autoresponse_msg = true;
  //ヘッダー情報
  $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;

  //自動返信の送信(結果を変数 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 );
  }
} else {
  //送信失敗時(もしあれば)
}
?>
<!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>お問い合わせフォーム</h2>
  <?php if ( $result ): ?>
  <h3>送信完了!</h3>
  <p>お問い合わせいただきありがとうございます。</p>
  <p>送信完了いたしました。</p>
    <?php if ( $show_autoresponse_msg ): ?>
      <?php if ( $result2 ): ?>
      <p>確認の自動返信メールを <?php echo $email; ?> へお送りいたしました。</p>
      <?php else: ?>
      <p>確認の自動返信メールを送信できませんでした。</p>
      <?php endif; ?>
    <?php endif; ?>
  <?php else: ?>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>ご迷惑をおかけして誠に申し訳ございません。</p>
  <?php endif; ?>
</div>
</body>
</html>

PHPMailer を使う

mb_send_mail() の代わりに PHP のライブラリ PHPMailer(6.5.1)を使ってメールを送信する例です。

入力ページや確認ページは前述までのものと同じなので省略します。

PHPMailer を別途インストールしておきます。また、メールサーバやメールアカウントのパスワードなどの情報を記述したファイル(phpmailvars.php)を作成し、外部からアクセスできないようにします。

以下がフォルダ構成です。この例の場合、PHPMailer は Composer を使ってインストールしています。

関連ページ:PHPMailer の使い方

├── contact
│   ├── contact.php  //入力ページ
│   ├── confirm.php  //確認ページ
│   ├── complete.php //完了ページ
│   └── formValidation.js //検証用 JavaScript
├── libs
│   ├── .htaccess //アクセス制御(このフォルダへの外部からのアクセスを拒否)
│   ├── functions.php //値を検証する関数やエスケープ処理をする関数のファイル
│   └── phpmailvars.php //メールアカウントのパスワードなどの情報を記述したファイル
├── php_mailer //PHPMailer
│   ├── composer.json
│   ├── composer.lock
│   └── vendor
│       ├── autoload.php
│       └── composer
│
└── style.css //スタイルシート
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 で行っています。

6行目は PHPMailer の読み込みです。Composer を使ってインストールしているので、Composer が提供する autoload.php を読み込んでいます。

メールの送信処理では PHPMailer のインスタンスを生成し(68行目)、そのプロパティやメソッドを使って送信処理の記述をします。

72行目はメインテナンス(デバグ)用の記述で、コメントアウトを外すとデバグの結果が全て出力されます。うまく送信できない場合などに利用します。

103行目のコメントアウトを外すと送信ができなかった際に PHPMailer によるエラーメッセージが表示されます。

complete.php PHP 部分抜粋
<?php
//セッションを開始
session_start();

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

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

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

//POSTされたデータをチェック
$_POST = checkInput( $_POST );
//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access denied' );
  }
} else {
  //トークンが存在しない場合(入力ページにリダイレクト)
  //die( 'Access Denied(直接このページにはアクセスできません)' );  //処理を中止する場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
  if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
    $_SERVER[ 'HTTPS' ] = 'on';
  }
  //入力画面(contact.php)の URL
  $url = ( empty( $_SERVER[ 'HTTPS' ] ) ? 'http://' : 'https://' ) . $_SERVER[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  exit;
}

//変数にエスケープ処理したセッション変数の値を代入
$name = h( $_SESSION[ 'name' ] );
$email = h( $_SESSION[ 'email' ] ) ;
$tel =  h( $_SESSION[ 'tel' ] ) ;
$subject = h( $_SESSION[ 'subject' ] );
$body = h( $_SESSION[ 'body' ] );

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

//-------- ★★★ PHPMailer を使ったメールの送信処理 ★★★ ------------

//PHPMailer 名前空間の使用(★PHPMailer 用に追加)
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

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

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

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 ポートを指定

  //日本語用
  $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");

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

} catch (Exception $e) {
  //例外が発生した場合はメール送信結果の変数 $result に false を代入
  $result = false;
  //PHPMailer のエラーを表示する場合
  //echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}

//メール送信の結果判定
if ( $result ) {
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去
  session_destroy(); //セッションを破棄
} else {
  //送信失敗時(もしあれば)
}
?>
<?php
//セッションを開始
session_start();

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

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

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

//POSTされたデータをチェック
$_POST = checkInput( $_POST );
//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access denied' );
  }
} else {
  //トークンが存在しない場合(入力ページにリダイレクト)
  //die( 'Access Denied(直接このページにはアクセスできません)' );  //処理を中止する場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
  if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
    $_SERVER[ 'HTTPS' ] = 'on';
  }
  //入力画面(contact.php)の URL
  $url = ( empty( $_SERVER[ 'HTTPS' ] ) ? 'http://' : 'https://' ) . $_SERVER[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  exit;
}

//変数にエスケープ処理したセッション変数の値を代入
$name = h( $_SESSION[ 'name' ] );
$email = h( $_SESSION[ 'email' ] ) ;
$tel =  h( $_SESSION[ 'tel' ] ) ;
$subject = h( $_SESSION[ 'subject' ] );
$body = h( $_SESSION[ 'body' ] );

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

//-------- ★★★ PHPMailer を使ったメールの送信処理 ★★★ ------------

//PHPMailer 名前空間の使用(★PHPMailer 用に追加)
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

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

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

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 ポートを指定

  //日本語用
  $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");

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

} catch (Exception $e) {
  //例外が発生した場合はメール送信結果の変数 $result に false を代入
  $result = false;
  //PHPMailer のエラーを表示する場合
  //echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}

//メール送信の結果判定
if ( $result ) {
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去
  session_destroy(); //セッションを破棄
} else {
  //送信失敗時(もしあれば)
}
?>
<!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>お問い合わせフォーム</h2>
  <?php if ( $result ): ?>
  <h3>送信完了!</h3>
  <p>お問い合わせいただきありがとうございます。</p>
  <p>送信完了いたしました。</p>
  <?php else: ?>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>ご迷惑をおかけして誠に申し訳ございません。</p>
  <?php endif; ?>
</div>
</body>
</html>

自動返信あり

以下は前述の例に自動返信の記述を追加した例です。

自動返信を追加する場合、新たに自動返信用のインスタンスを生成してそのメソッドやプロパティを使います(57行目〜)。

complete.php メール送信及び自動返信部分抜粋
//-------- ★★★ PHPMailer を使ったメールの送信処理 ★★★ ------------

//PHPMailer 名前空間の使用(★PHPMailer 用に追加)
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

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

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

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 ポートを指定

  //日本語用
  $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");

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

} catch (Exception $e) {
  $result = false;
  //PHPMailer のエラーを表示する場合
  //echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}

//メール送信の結果判定
if ( $result ) {
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去
  session_destroy(); //セッションを破棄

  //★★★ 自動返信メール ★★★
  //自動返信のインスタンスを生成
  $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 ); // テキストメール
    $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}";
  }
} else {
  //送信失敗時(もしあれば)
}
<?php
//セッションを開始
session_start();

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

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

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

//POSTされたデータをチェック
$_POST = checkInput( $_POST );
//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access denied' );
  }
} else {
  //トークンが存在しない場合(入力ページにリダイレクト)
  //die( 'Access Denied(直接このページにはアクセスできません)' );  //処理を中止する場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
  if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
    $_SERVER[ 'HTTPS' ] = 'on';
  }
  //入力画面(contact.php)の URL
  $url = ( empty( $_SERVER[ 'HTTPS' ] ) ? 'http://' : 'https://' ) . $_SERVER[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  exit;
}

//変数にエスケープ処理したセッション変数の値を代入
$name = h( $_SESSION[ 'name' ] );
$email = h( $_SESSION[ 'email' ] ) ;
$tel =  h( $_SESSION[ 'tel' ] ) ;
$subject = h( $_SESSION[ 'subject' ] );
$body = h( $_SESSION[ 'body' ] );

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

//-------- ★★★ PHPMailer を使ったメールの送信処理 ★★★ ------------

//PHPMailer 名前空間の使用(★PHPMailer 用に追加)
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

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

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

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 ポートを指定

  //日本語用
  $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");

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

} catch (Exception $e) {
  $result = false;
  //PHPMailer のエラーを表示する場合
  //echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}

//メール送信の結果判定
if ( $result ) {
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去
  session_destroy(); //セッションを破棄

  //★★★ 自動返信メール ★★★
  //自動返信のインスタンスを生成
  $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 ); // テキストメール
    $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}";
  }
} else {
  //送信失敗時(もしあれば)
}
?>
<!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>お問い合わせフォーム</h2>
  <?php if ( $result ): ?>
  <h3>送信完了!</h3>
  <p>お問い合わせいただきありがとうございます。</p>
  <p>送信完了いたしました。</p>
    <?php if ( $result2 ): ?>
    <p>確認の自動返信メールを <?php echo $email; ?> へお送りいたしました。</p>
    <?php else: ?>
    <p>確認の自動返信メールを送信できませんでした。</p>
    <?php endif; ?>
  <?php else: ?>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>ご迷惑をおかけして誠に申し訳ございません。</p>
  <?php endif; ?>
</div>
</body>
</html>

Gmail の SMTP サーバを使用

以下は Gmail の SMTP 認証で XOAUTH2 を使う例です。

XOAUTH2 を使えば、「安全性の低いアプリの許可」を有効にせずに Gmail の SMTP サーバを使用することができます。

但し、XOAUTH2 のライブラリを使用するには PHPMailer 以外に oauth2-client(oauth2-google)のインストールとGoogle API プロジェクトの登録(割と手間がかかります)が必要です。

関連ページ:Gmail の SMTP 認証で XOAUTH2 を使う

以下の例の場合、前述の PHPMailer を使う例とフォルダ構成が異なります。

├── contact_Gmail  //メインのフォルダ
│   ├── complete.php  //完了ページ
│   ├── confirm.php   //確認ページ
│   ├── contact.php   //入力ページ
│   ├── formValidation.js   //検証用 JavaScript
│   └── phpmailer //PHPMailer と依存ライブラリ
│       ├── composer.json
│       ├── composer.lock
│       └── vendor
│           ├── autoload.php
│           ├── composer
│           ├── guzzlehttp
│           ├── league
│           ・・・以下省略・・・
├── libs
│   ├── .htaccess //アクセス制御(このフォルダへの外部からのアクセスを拒否)
│   ├── functions.php
│   └── phpmailvars_oauth2.php //Gmailのアカウントの情報
└── style.css //スタイルシート

Gmail の SMTP 認証で XOAUTH2 を使う場合に必要になるクライアント ID やクライアントシークレット、トークンの情報などは別ファイルに記述して、外部からアクセスできない場所に保存します。

phpmailvars_oauth2.php
<?php
//SMTP サーバの指定(Gmail)
define('MAIL_HOST', 'smtp.gmail.com');
//Gmail アドレス(この例の場合は、送信元と送信先、及び自動送信の送信元にこのアドレスを使用)
define('GMAIL_ADDRESS', 'xxxxxx@gmail.com');
//Gmail のアカウント名
define('GMAIL_ACCT_NAME', '〇〇〇〇');
//クライアント ID
define('CLIENT_ID', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
//クライアントシークレット
define('CLIENT_SECRET', 'xxxxxxxxxxxxxxxxxxxx');
//トークン(Refresh Token)
define('REFRESH_TOKEN', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
//Bcc
define('BCC', 'xxxxxx@xxxxxx.com');

以下は PHPMailer を使って Gmail のアカウントでメールを送信する例です。

complete.php Gmail SMTP でのメール送信部分抜粋
//---- ★★★ PHPMailer を使ったメールの送信処理(Gmailサーバ) ★★★ ----
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\OAuth; //  ### 追加 ###

// Alias the League Google OAuth2 provider class
use League\OAuth2\Client\Provider\Google;//  ### 追加 ###

//PHPMailer の読み込み(PHPMailer の位置により適宜変更)
require 'phpmailer/vendor/autoload.php';
//Gmail の SMTP 認証情報(クライアント ID等)の読み込み
require '../libs/phpmailvars_oauth2.php';

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

// ###  OAUTH2 設定に使う値を変数に代入(値は phpmailvars_oauth2.php)  ###
//Gmail メールアドレス
$google_email = GMAIL_ADDRESS;
//クライアント ID
$clientId = CLIENT_ID;
//クライアントシークレット
$clientSecret = CLIENT_SECRET;
//トークン(Refresh Token)
$refreshToken = REFRESH_TOKEN;

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

// ###  OAUTH2 の設定  ###
//OAuth2 プロバイダのインスタンスの生成
$provider = new Google(
  [
    'clientId' => $clientId,
    'clientSecret' => $clientSecret,
  ]
);
//送信結果の真偽値の初期化
$result = false;
$result2 = false;

try {
  //サーバ設定
  //$mail->SMTPDebug = SMTP::DEBUG_SERVER; // デバグの出力を有効に
  $mail->isSMTP(); // SMTP を使用
  // SMTP サーバーを指定
  $mail->Host = MAIL_HOST;
  // SMTP authentication を有効に
  $mail->SMTPAuth = true;
  //AuthType を XOAUTH2 に指定
  $mail->AuthType = 'XOAUTH2';
  // 暗号化(TLS)を有効に
  $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
  //ポートの指定
  $mail->Port = 587;

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

  // ###  OAUTH2 の設定  ###
  //OAuth プロバイダのインスタンスを PHPMailer へ渡す
  $mail->setOAuth(
    new OAuth(
      [
        'provider' => $provider,
        'clientId' => $clientId,
        'clientSecret' => $clientSecret,
        'refreshToken' => $refreshToken,
        'userName' => $google_email,
      ]
    )
  );

  //受信者設定
  //差出人アドレス, 差出人名(差出人アドレスには Gmail アカウントのアドレスを指定)
  $mail->setFrom($google_email, mb_encode_mimeheader(GMAIL_ACCT_NAME));
  //送信先アドレス (この例の場合は Gmail のアドレス)・宛先名
  $mail->AddAddress($google_email, mb_encode_mimeheader(GMAIL_ACCT_NAME));
  //返信アドレスに差出人(お問い合わせをしたユーザ)を指定
  $mail->addReplyTo($email, mb_encode_mimeheader($name));
  //Bcc アドレス
  $mail->AddBcc( BCC );
  // テキスト形式メール
  $mail->isHTML( false );
  //件名
  $mail->Subject = mb_encode_mimeheader( $subject );
  //70 文字で改行(好みで)
  $mail->WordWrap = 70;
  //本文
  $mail->Body = mb_convert_encoding( $mail_body, "JIS", "UTF-8" );

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

} catch ( Exception $e ) {
  $result = false;
  //echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}

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

  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去
  session_destroy(); //セッションを破棄

  //自動返信メール
  $autoresponder = new PHPMailer( true );
  try {
    //サーバ設定
    //$autoresponder->SMTPDebug = SMTP::DEBUG_SERVER; // デバグの出力を有効に
    // SMTP を使用
    $autoresponder->isSMTP();
    // Gmail SMTP サーバーを指定
    $autoresponder->Host = MAIL_HOST;
    // SMTP authentication を有効に
    $autoresponder->SMTPAuth = true;
    //AuthType を XOAUTH2 に指定
    $autoresponder->AuthType = 'XOAUTH2';
    $autoresponder->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
    $autoresponder->Port = 587;

    // ###  OAUTH2 の設定  ###
    //OAuth プロバイダのインスタンスを PHPMailer へ渡す
    $autoresponder->setOAuth(
      new OAuth(
        [
          'provider' => $provider,
          'clientId' => $clientId,
          'clientSecret' => $clientSecret,
          'refreshToken' => $refreshToken,
          'userName' => $google_email,
        ]
      )
    );

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

    //受信者設定
    //差出人アドレス, 差出人名(差出人アドレスには Gmail アカウントのアドレスを指定 )
    $autoresponder->setFrom($google_email, mb_encode_mimeheader(GMAIL_ACCT_NAME));
    //送信先・宛先名(お問い合わせしたユーザ)
    $autoresponder->AddAddress( $email, mb_encode_mimeheader( $name ) );
    // テキスト形式メール
    $autoresponder->isHTML( false );
    //件名
    $autoresponder->Subject = mb_encode_mimeheader( "自動返信メール" );
    //返信用アドレス
    $autoresponder->addReplyTo( $google_email, 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 ) {
    echo "Auto Response Message could not be sent. Mailer Error: {$autoresponder->ErrorInfo}";
  }
} else {
  //送信失敗時(もしあれば)
}
<?php
//セッションを開始
session_start();

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

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

//POSTされたデータをチェック
$_POST = checkInput( $_POST );
//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access denied' );
  }
} else {
  //トークンが存在しない場合(入力ページにリダイレクト)
  //die( 'Access Denied(直接このページにはアクセスできません)' );  //処理を中止する場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  //サーバー変数 $_SERVER['HTTPS'] が取得出来ない環境用
  if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] === "https") {
    $_SERVER[ 'HTTPS' ] = 'on';
  }
  //入力画面(contact.php)の URL
  $url = ( empty( $_SERVER[ 'HTTPS' ] ) ? 'http://' : 'https://' ) . $_SERVER[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  exit;
}

//変数にエスケープ処理したセッション変数の値を代入
$name = h( $_SESSION[ 'name' ] );
$email = h( $_SESSION[ 'email' ] ) ;
$tel =  h( $_SESSION[ 'tel' ] ) ;
$subject = h( $_SESSION[ 'subject' ] );
$body = h( $_SESSION[ 'body' ] );

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

//-------- ★★★ PHPMailer を使ったメールの送信処理(Gmailサーバ) ★★★ ------------
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\OAuth; //  ### 追加 ###

// Alias the League Google OAuth2 provider class
use League\OAuth2\Client\Provider\Google;//  ### 追加 ###

//PHPMailer の読み込み(PHPMailer の位置により適宜変更)
require 'phpmailer/vendor/autoload.php';
//Gmail の SMTP 認証情報(クライアント ID等)の読み込み
require '../libs/phpmailvars_oauth2.php';

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

// ###  OAUTH2 設定に使う値を変数に代入(値は phpmailvars_oauth2.php)   ###
//Gmail メールアドレス
$google_email = GMAIL_ADDRESS;
//クライアント ID
$clientId = CLIENT_ID;
//クライアントシークレット
$clientSecret = CLIENT_SECRET;
//トークン(Refresh Token)
$refreshToken = REFRESH_TOKEN;

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

// ###  OAUTH2 の設定  ###
//OAuth2 プロバイダのインスタンスの生成
$provider = new Google(
  [
    'clientId' => $clientId,
    'clientSecret' => $clientSecret,
  ]
);
//送信結果の真偽値の初期化
$result = false;
$result2 = false;

try {
  //サーバ設定
  //$mail->SMTPDebug = SMTP::DEBUG_SERVER; // デバグの出力を有効に
  $mail->isSMTP(); // SMTP を使用
  // SMTP サーバーを指定
  $mail->Host = MAIL_HOST;
  // SMTP authentication を有効に
  $mail->SMTPAuth = true;
  //AuthType を XOAUTH2 に指定
  $mail->AuthType = 'XOAUTH2';
  // 暗号化(TLS)を有効に
  $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
  //ポートの指定
  $mail->Port = 587;

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

  // ###  OAUTH2 の設定  ###
  //OAuth プロバイダのインスタンスを PHPMailer へ渡す
  $mail->setOAuth(
    new OAuth(
      [
        'provider' => $provider,
        'clientId' => $clientId,
        'clientSecret' => $clientSecret,
        'refreshToken' => $refreshToken,
        'userName' => $google_email,
      ]
    )
  );

  //受信者設定
  //差出人アドレス, 差出人名(差出人アドレスには Gmail アカウントのアドレスを指定)
  $mail->setFrom($google_email, mb_encode_mimeheader(GMAIL_ACCT_NAME));
  //送信先アドレス (この例の場合は Gmail のアドレス)・宛先名
  $mail->AddAddress($google_email, mb_encode_mimeheader(GMAIL_ACCT_NAME));
  //返信アドレスに差出人(お問い合わせをしたユーザ)を指定
  $mail->addReplyTo($email, mb_encode_mimeheader($name));
  //Bcc アドレス
  $mail->AddBcc( BCC );
  // テキスト形式メール
  $mail->isHTML( false );
  //件名
  $mail->Subject = mb_encode_mimeheader( $subject );
  //70 文字で改行(好みで)
  $mail->WordWrap = 70;
  //本文
  $mail->Body = mb_convert_encoding( $mail_body, "JIS", "UTF-8" );

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

} catch ( Exception $e ) {
  $result = false;
  //echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}

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

  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去
  session_destroy(); //セッションを破棄

  //自動返信メール
  $autoresponder = new PHPMailer( true );
  try {
    //サーバ設定
    //$autoresponder->SMTPDebug = SMTP::DEBUG_SERVER; // デバグの出力を有効に
    // SMTP を使用
    $autoresponder->isSMTP();
    // Gmail SMTP サーバーを指定
    $autoresponder->Host = MAIL_HOST;
    // SMTP authentication を有効に
    $autoresponder->SMTPAuth = true;
    //AuthType を XOAUTH2 に指定
    $autoresponder->AuthType = 'XOAUTH2';
    $autoresponder->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
    $autoresponder->Port = 587;

    // ###  OAUTH2 の設定  ###
    //OAuth プロバイダのインスタンスを PHPMailer へ渡す
    $autoresponder->setOAuth(
      new OAuth(
        [
          'provider' => $provider,
          'clientId' => $clientId,
          'clientSecret' => $clientSecret,
          'refreshToken' => $refreshToken,
          'userName' => $google_email,
        ]
      )
    );

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

    //受信者設定
    //差出人アドレス, 差出人名(差出人アドレスには Gmail アカウントのアドレスを指定 )
    $autoresponder->setFrom($google_email, mb_encode_mimeheader(GMAIL_ACCT_NAME));
    //送信先・宛先名(お問い合わせしたユーザ)
    $autoresponder->AddAddress( $email, mb_encode_mimeheader( $name ) );
    // テキスト形式メール
    $autoresponder->isHTML( false );
    //件名
    $autoresponder->Subject = mb_encode_mimeheader( "自動返信メール" );
    //返信用アドレス
    $autoresponder->addReplyTo( $google_email, 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 ) {
    echo "Auto Response Message could not be sent. Mailer Error: {$autoresponder->ErrorInfo}";
  }
} else {
  //送信失敗時(もしあれば)
}
?>
<!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>お問い合わせフォーム</h2>
  <?php if ( $result ): ?>
  <h3>送信完了!</h3>
  <p>お問い合わせいただきありがとうございます。</p>
  <p>送信完了いたしました。</p>
    <?php if ( $result2 ): ?>
    <p>確認の自動返信メールを <?php echo $email; ?> へお送りいたしました。</p>
    <?php else: ?>
    <p>確認の自動返信メールを送信できませんでした。</p>
    <?php endif; ?>
  <?php else: ?>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>ご迷惑をおかけして誠に申し訳ございません。</p>
  <?php endif; ?>
</div>
</body>
</html>