お問い合わせフォームを作ろう

目次

お問い合わせフォームを作ろう

プライバシーポリシー、免責事項は作ったので、Googleアドセンスに必要なお問い合わせフォームを作ることに。

送信ボタンを押しても、POST処理がおこなわれない問題に嵌まる

HTML(contact.php)
<form id="contact" method="POST" class="border-2 border-teal-300 rounded-xl p-4">
    <div>お名前<span class="text-red-500">*</span></div>
    <input type="text" name="customer_name" placeholder="山田 太郎" required class="block border border-gray-800 p-2 rounded-lg mb-4">
    <div>メールアドレス<span class="text-red-500">*</span></div>
    <input type="email" name="customer_mail" placeholder="abc@example.com" required class="block border border-gray-800 p-2 rounded-lg mb-4
    ">
    <div>本文<span class="text-red-500">*</span></div>
    <textarea name="mail_text" required class="block px-3 py-6 w-full border border-gray-800 rounded-lg mb-4"></textarea>
    <button type="submit" id="sendBtn" name="sendBtn" class="block mt-4 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">送信</button>
</form>
PHP
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  var_dump('test');
  exit;
}

よーし、書けた!

が、送信ボタンを押しても動かない。

どうしてか調べたところ、

action=”/contact.php”がformに書いていない

②素のPHPなのにMVC的に作ったから、Controller側での処理を書かなければならない

ということが原因だった。

PHP(public/contact.php)
<?php
require __DIR__ . '/../app/Controllers/ContactController.php';

$controller = new ContactController();
$controller->handle();
?>
PHP(Controller)
<?php
class ContactController {

  // POSTかどうか判定
  public function handle()
  {
      if ($_SERVER['REQUEST_METHOD'] === 'POST') {
          $this->send();
      } else {
          $this->index();
      }
  }

  // フォーム表示
  public function index() {

    require __DIR__ . '/../Views/contact.php';
  }
  
  // 送信処理
  private function send()
  {
    var_dump('test');
    exit;
  }
?>
HTML(Views/contact.php)
<form id="contact" method="POST" action="/contact.php" class="border-2 border-teal-300 rounded-xl p-4">

Controllerの使い方をすっかり忘れていた・・・

こんなコード書いても、いくら送信ボタンを押しても動かないのは当然すぎる(恥)

CSRFトークンを入れよう

管理画面には全部入れていたけれど、念のためお問い合わせフォームにもCSRFトークンを入れることに。

PHP(helpers/csrf.php)
<?php

if (session_status() === PHP_SESSION_NONE) {
    session_start();
}

// トークン生成
function generateCsrfToken(): string
{
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

// トークン確認
function validateCsrfToken(string $token): void
{
    if (
        empty($_SESSION['csrf_token']) ||
        !hash_equals($_SESSION['csrf_token'], $token)
    ) {
        exit('不正なリクエストです(CSRF)');
    }
}
?>
PHP(Controller)
<?php
require_once __DIR__ . '/../helpers/csrf.php';

  // フォーム表示
  public function index() {

    // CSRFトークン生成
    $csrfToken = generateCsrfToken();

    require __DIR__ . '/../Views/contact.php';
  }

  // 送信処理
  private function send()
  {
    // CSRFチェック
    validateCsrfToken($_POST['csrf_token'] ?? '');
  }
}
HTML(Views/contact.php)
<form id="contact" method="POST" action="/contact.php" class="border-2 border-teal-300 rounded-xl p-4">
  <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES) ?>">
  ...
</form>

ちなみに ENT_QUOTES とは「特殊文字のうちシングルクオートとダブルクオートを共に変換する」という意味のよう。

送信後に戻ると再送信されてしまう問題に立ち向かう

送信後に戻るボタンを押を押したり、更新ボタンを押してしまうと、

メールが再送信されることがあるので、それの予防策も入れていこうと思います。

今回は PRG(Post / Redirect / Get)パターン を使います。

POST /contact.php

送信処理

redirect(Location)

GET /contact/thanks.php

の流れで作っていきます。

HTML(Views/contact_thanks.php)
<?php
$title = 'お問い合わせ完了';
require __DIR__ . '/partials/header.php';
?>

<h1 class="text-xl font-bold mb-4">お問い合わせありがとうございました</h1>

<p>
  内容を確認の上、必要に応じてご連絡いたします。
</p>

<a href="/" class="inline-block mt-6 text-blue-600 underline">
  トップへ戻る
</a>

<?php require __DIR__ . '/partials/footer.php'; ?>
PHP(Controller)
public function thanks()
{
    require __DIR__ . '/../Views/contact_thanks.php';
}

// 送信処理
private function send()
{
  // CSRFチェック
  validateCsrfToken($_POST['csrf_token'] ?? '');

  // 二重送信防止のためトークン破棄(任意だが推奨)
  unset($_SESSION['csrf_token']);

  // 送信完了画面に転送
  header('Location: /contact_thanks.php');
  exit;
}
PHP(public/contact_thanks.php)
<?php
require __DIR__ . '/../app/Controllers/ContactController.php';

$controller = new ContactController();
$controller->thanks();

最後に表示されたページは GET になるので、ブラウザの履歴には POSTが残らない
それに、トークンもunsetしてしまえば、同じトークンでの再送信も不可能になる。
よって、戻るor更新しても再送信はできない。

バリデーションを入れよう

必要な形式を満たしているかどうかチェックするバリデーションも入れます。

名前とメールアドレスは100文字、本文は1000文字までにしています。

PHP(Controller)
private function send()
{
    validateCsrfToken($_POST['csrf_token'] ?? '');

    $errors = [];
    $old = [];

    // 値を取得&保持用
    $name  = trim($_POST['customer_name'] ?? '');
    $email = trim($_POST['customer_mail'] ?? '');
    $text  = trim($_POST['mail_text'] ?? '');

    $old = [
        'customer_name' => $name,
        'customer_mail' => $email,
        'mail_text'     => $text,
    ];

    // バリデーション
    if ($name === '') {
        $errors['customer_name'] = 'お名前は必須です';
    } elseif (mb_strlen($name) > 100) {
        $errors['customer_name'] = 'お名前は100文字以内で入力してください';
    }

    if ($email === '') {
        $errors['customer_mail'] = 'メールアドレスは必須です';
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors['customer_mail'] = 'メールアドレスの形式が正しくありません';
    }

    if ($text === '') {
        $errors['mail_text'] = '本文は必須です';
    } elseif (mb_strlen($text) > 1000) {
        $errors['mail_text'] = '本文は1000文字以内で入力してください';
    }

    // エラーがある場合:フォームに戻す
    if (!empty($errors)) {
        // CSRFトークン再生成
        $csrfToken = generateCsrfToken();

        require __DIR__ . '/../Views/contact.php';
        return;
    }

    // ===== 成功時 =====

    // トークン破棄(二重送信防止)
    unset($_SESSION['csrf_token']);

    header('Location: /contact_thanks.php');
    exit;
}
HTML(Views/contact.php)
<form method="POST" action="/contact.php">

  <input type="hidden" name="csrf_token"
         value="<?= htmlspecialchars($csrfToken, ENT_QUOTES) ?>">

  <!-- お名前 -->
  <div>お名前<span class="text-red-500">*</span></div>
  <?php if (!empty($errors['customer_name'])): ?>
    <p class="text-red-500 text-sm"><?= htmlspecialchars($errors['customer_name']) ?></p>
  <?php endif; ?>
  <input type="text" name="customer_name"
         value="<?= htmlspecialchars($old['customer_name'] ?? '') ?>"
         class="block border p-2 mb-4">

  <!-- メール -->
  <div>メールアドレス<span class="text-red-500">*</span></div>
  <?php if (!empty($errors['customer_mail'])): ?>
    <p class="text-red-500 text-sm"><?= htmlspecialchars($errors['customer_mail']) ?></p>
  <?php endif; ?>
  <input type="email" name="customer_mail"
         value="<?= htmlspecialchars($old['customer_mail'] ?? '') ?>"
         class="block border p-2 mb-4">

  <!-- 本文 -->
  <div>本文<span class="text-red-500">*</span></div>
  <?php if (!empty($errors['mail_text'])): ?>
    <p class="text-red-500 text-sm"><?= htmlspecialchars($errors['mail_text']) ?></p>
  <?php endif; ?>
  <textarea name="mail_text"
            class="block border p-2 mb-4"><?= htmlspecialchars($old['mail_text'] ?? '') ?></textarea>

  <button type="submit">送信</button>
</form>

redirect すると $errors が消えてしまうので、同じリクエスト内で View を表示する。
CSRFトークンを再生成するのは、エラー後も再送信でき、トークン切れを防ぐ役割がある。

実際にメールを送るロジックを入れよう

ここがこの記事の真骨頂。

全てController側で制御します。

初めにControllerの最初かsendクラスの最初に下記を記述

PHP(Controller)
mb_language('Japanese');
mb_internal_encoding('UTF-8');

次に管理者宛て用のメール本文を作る

PHP(Controller)
// 管理者宛
$to = 'admin@example.com'; // ←あなたの受信メール
$subject = '【お問い合わせ】〇〇サイト';

// 本文(ヒアドキュメント)
$body = <<<EOT
〇〇サイトからお問い合わせがありました。

【お名前】
{$name}

【メールアドレス】
{$email}

【本文】
{$text}

EOT;

次に、どこからメールが来たかのヘッダー情報を作る。

PHP(Controller)
$headers = implode("\r\n", [
    'From: '.$email, // noreply@yourdomain.comとかでもOK
    'Reply-To: '.$email,
    'Content-Type: text/plain; charset=UTF-8',
]);

最後にメール送信処理を入れる。

PHP(Controller)
$result = mb_send_mail($to, $subject, $body, $headers);

if (!$result) {
    // 失敗時(ログに残すのがおすすめ)
    error_log('お問い合わせメール送信失敗');
}

これでメール送信処理は一応終了です。

お疲れさまでした!

但し、ローカル環境ではメールは届きませんので、本番環境で試してみてくださいね。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次