お問い合わせフォームを作ろう
プライバシーポリシー、免責事項は作ったので、Googleアドセンスに必要なお問い合わせフォームを作ることに。
送信ボタンを押しても、POST処理がおこなわれない問題に嵌まる
<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>if ($_SERVER['REQUEST_METHOD'] === 'POST') {
var_dump('test');
exit;
}よーし、書けた!
が、送信ボタンを押しても動かない。
どうしてか調べたところ、
①action=”/contact.php”がformに書いていない
②素のPHPなのにMVC的に作ったから、Controller側での処理を書かなければならない
ということが原因だった。
<?php
require __DIR__ . '/../app/Controllers/ContactController.php';
$controller = new ContactController();
$controller->handle();
?><?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;
}
?><form id="contact" method="POST" action="/contact.php" class="border-2 border-teal-300 rounded-xl p-4">Controllerの使い方をすっかり忘れていた・・・
こんなコード書いても、いくら送信ボタンを押しても動かないのは当然すぎる(恥)
CSRFトークンを入れよう
管理画面には全部入れていたけれど、念のためお問い合わせフォームにもCSRFトークンを入れることに。
<?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
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'] ?? '');
}
}<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
の流れで作っていきます。
<?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'; ?>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
require __DIR__ . '/../app/Controllers/ContactController.php';
$controller = new ContactController();
$controller->thanks();最後に表示されたページは GET になるので、ブラウザの履歴には POSTが残らない。
それに、トークンもunsetしてしまえば、同じトークンでの再送信も不可能になる。
よって、戻るor更新しても再送信はできない。
バリデーションを入れよう
必要な形式を満たしているかどうかチェックするバリデーションも入れます。
名前とメールアドレスは100文字、本文は1000文字までにしています。
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;
}<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クラスの最初に下記を記述
mb_language('Japanese');
mb_internal_encoding('UTF-8');次に管理者宛て用のメール本文を作る
// 管理者宛
$to = 'admin@example.com'; // ←あなたの受信メール
$subject = '【お問い合わせ】〇〇サイト';
// 本文(ヒアドキュメント)
$body = <<<EOT
〇〇サイトからお問い合わせがありました。
【お名前】
{$name}
【メールアドレス】
{$email}
【本文】
{$text}
EOT;次に、どこからメールが来たかのヘッダー情報を作る。
$headers = implode("\r\n", [
'From: '.$email, // noreply@yourdomain.comとかでもOK
'Reply-To: '.$email,
'Content-Type: text/plain; charset=UTF-8',
]);最後にメール送信処理を入れる。
$result = mb_send_mail($to, $subject, $body, $headers);
if (!$result) {
// 失敗時(ログに残すのがおすすめ)
error_log('お問い合わせメール送信失敗');
}これでメール送信処理は一応終了です。
お疲れさまでした!
但し、ローカル環境ではメールは届きませんので、本番環境で試してみてくださいね。
