【PHPバリデーション入門】サーバー側で入力チェックしてエラーを表示する

章: 2章

技術タグ: DevTools JSON PHP POST セキュリティ バリデーション フォーム

今回やること

前回は、送られてきた値をそのまま表示するとXSSの危険があることを体験しました。

そして、htmlspecialchars() を使って、画面に出す時にエスケープする必要があることを学びました。

今回は、さらにフォーム処理を実務に近づけます。

PHP側で入力チェックを行い、エラーがあればフォーム画面に戻して、エラーメッセージを表示します。

フォーム送信
↓
PHPで受け取る
↓
PHPで入力チェックする
↓
エラーがあればフォームへ戻す
↓
エラーメッセージを表示する
↓
問題なければ確認画面を表示する

ここで重要なのは、JavaScriptに頼らず、サーバー側で必ずチェックすることです。


今回のゴール

今回のゴールは、以下です。

  • PHP側で空欄チェックをする
  • PHP側でメールアドレス形式をチェックする
  • エラーがあればフォーム画面へ戻す
  • エラーメッセージを表示する
  • 入力済みの値をフォームに戻す
  • 正常な時だけ送信内容確認画面を表示する
  • 出力時エスケープを継続する

今回で、フォーム処理はかなり実務の形に近づきます。


なぜサーバー側バリデーションが必要なのか

JavaScriptの入力チェックは便利です。

しかし、前回までに見た通り、ブラウザ側の処理はユーザーに見えます。

DevToolsで書き換えられる可能性もあります。

curlを使えば、そもそもフォーム画面を通らずに直接POSTすることもできます。

だから、PHP側では必ず入力チェックをします。

JavaScriptのチェック
↓
ユーザー補助

PHPのチェック
↓
最終防衛ライン

この役割分担を忘れないことが大事です。


今回のファイル構成

今回も以下の3ファイルで進めます。

contact-form-app/
├─ index.php
├─ receive.php
└─ style.css
ファイル役割
index.phpフォーム表示・エラー表示・入力値の再表示
receive.phpPOST受信・PHPバリデーション・成功時の確認表示
style.cssフォーム・エラー・確認画面の見た目を整える

前回のコードをベースに、PHP側のチェックとエラー表示を追加します。


index.phpを修正する

まず、フォーム画面でエラーメッセージを表示できるようにします。

index.php を以下の内容にします。

<?php
/**
 * index.php
 *
 * 役割:
 * - お問い合わせフォームを表示する
 * - GETパラメータで渡されたエラーメッセージを表示する
 * - 送信先の receive.php へPOST送信する
 */

$errors = $_GET['errors'] ?? [];
$name = $_GET['name'] ?? '';
$email = $_GET['email'] ?? '';
$message = $_GET['message'] ?? '';

if (!is_array($errors)) {
    $errors = [];
}

/**
 * HTML出力用に文字列をエスケープする
 *
 * @param string $value 画面に表示したい文字列
 * @return string エスケープ済みの文字列
 */
function escape_html($value)
{
    return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>お問い合わせフォーム</title>
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <main class="contact-page">
    <h1 class="contact-page__title">お問い合わせフォーム</h1>

    <p class="contact-page__lead">
      ご質問やご相談があれば、以下のフォームから送信してください。
    </p>

    <?php if (!empty($errors)) : ?>
      <div class="error-box">
        <p class="error-box__title">入力内容を確認してください。</p>
        <ul class="error-box__list">
          <?php foreach ($errors as $error) : ?>
            <li><?php echo escape_html($error); ?></li>
          <?php endforeach; ?>
        </ul>
      </div>
    <?php endif; ?>

    <form class="contact-form" action="receive.php" method="post">
      <div class="contact-form__field">
        <label class="contact-form__label" for="name">お名前</label>
        <input class="contact-form__control" type="text" id="name" name="name" value="<?php echo escape_html($name); ?>" />
      </div>

      <div class="contact-form__field">
        <label class="contact-form__label" for="email">メールアドレス</label>
        <input class="contact-form__control" type="email" id="email" name="email" value="<?php echo escape_html($email); ?>" />
      </div>

      <div class="contact-form__field">
        <label class="contact-form__label" for="message">お問い合わせ内容</label>
        <textarea class="contact-form__control contact-form__textarea" id="message" name="message"><?php echo escape_html($message); ?></textarea>
      </div>

      <button class="contact-form__button" type="submit">
        送信する
      </button>
    </form>
  </main>
</body>
</html>

今回の index.php は、フォームを表示するだけではありません。

エラーがある場合は、画面上部にエラーメッセージを表示します。

また、エラーで戻ってきた時に、入力済みの値をフォームに戻します。


エラー配列を受け取る

index.php の先頭では、GETパラメータからエラーや入力値を受け取っています。

$errors = $_GET['errors'] ?? [];
$name = $_GET['name'] ?? '';
$email = $_GET['email'] ?? '';
$message = $_GET['message'] ?? '';

今回は、エラーがあった場合に receive.php から index.php へリダイレクトします。

その時に、エラーメッセージと入力値をURLパラメータとして渡します。

これはMVPとして分かりやすくするための方法です。

本格的には、セッションを使ってエラーや入力値を渡す方法もあります。


入力済みの値をフォームに戻す

エラー時に入力内容が全部消えると、ユーザー体験が悪くなります。

そこで、入力済みの値をフォームに戻します。

value="<?php echo escape_html($name); ?>"

お問い合わせ内容の textarea も同じです。

<textarea name="message"><?php echo escape_html($message); ?></textarea>

ここでも、必ず escape_html() を通しています。

ユーザーが入力した値をHTMLに出すので、出力時エスケープが必要です。


receive.phpを修正する

次に、POSTを受け取る receive.php を修正します。

receive.php を以下の内容にします。

<?php
/**
 * receive.php
 *
 * 役割:
 * - フォームからPOST送信された値を受け取る
 * - サーバー側で入力チェックする
 * - エラーがあればフォーム画面へ戻す
 * - 問題なければ送信内容を表示する
 */

$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$message = trim($_POST['message'] ?? '');

$errors = [];

/**
 * HTML出力用に文字列をエスケープする
 *
 * @param string $value 画面に表示したい文字列
 * @return string エスケープ済みの文字列
 */
function escape_html($value)
{
    return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}

if ($name === '') {
    $errors[] = 'お名前を入力してください。';
}

if ($email === '') {
    $errors[] = 'メールアドレスを入力してください。';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $errors[] = 'メールアドレスの形式で入力してください。';
}

if ($message === '') {
    $errors[] = 'お問い合わせ内容を入力してください。';
}

if (!empty($errors)) {
    $query = http_build_query([
        'errors' => $errors,
        'name' => $name,
        'email' => $email,
        'message' => $message,
    ]);

    header('Location: index.php?' . $query);
    exit;
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>送信内容の確認</title>
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <main class="contact-page">
    <h1 class="contact-page__title">送信内容の確認</h1>

    <div class="result-box">
      <div class="result-box__item">
        <p class="result-box__label">お名前</p>
        <p class="result-box__value"><?php echo escape_html($name); ?></p>
      </div>

      <div class="result-box__item">
        <p class="result-box__label">メールアドレス</p>
        <p class="result-box__value"><?php echo escape_html($email); ?></p>
      </div>

      <div class="result-box__item">
        <p class="result-box__label">お問い合わせ内容</p>
        <p class="result-box__value"><?php echo escape_html($message); ?></p>
      </div>

      <a class="result-box__link" href="index.php">フォームへ戻る</a>
    </div>
  </main>
</body>
</html>

今回の中心は、PHP側で入力チェックをする部分です。

if ($name === '') {
    $errors[] = 'お名前を入力してください。';
}

if ($email === '') {
    $errors[] = 'メールアドレスを入力してください。';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $errors[] = 'メールアドレスの形式で入力してください。';
}

if ($message === '') {
    $errors[] = 'お問い合わせ内容を入力してください。';
}

空欄チェックだけでなく、メールアドレスの形式チェックも行っています。


filter_var()でメール形式を確認する

メールアドレスの形式チェックには、PHPの filter_var() を使っています。

filter_var($email, FILTER_VALIDATE_EMAIL)

これは、文字列がメールアドレス形式として妥当か確認するためのPHP標準機能です。

完璧なメール到達確認ではありませんが、入力形式チェックとしては便利です。

今回は、空欄でない場合だけメール形式チェックをしています。

if ($email === '') {
    $errors[] = 'メールアドレスを入力してください。';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $errors[] = 'メールアドレスの形式で入力してください。';
}

空欄チェックと形式チェックを分けることで、エラーメッセージを分かりやすくできます。


エラーがあればフォームへ戻す

エラーが1つでもあれば、確認画面を表示せずにフォームへ戻します。

if (!empty($errors)) {
    $query = http_build_query([
        'errors' => $errors,
        'name' => $name,
        'email' => $email,
        'message' => $message,
    ]);

    header('Location: index.php?' . $query);
    exit;
}

http_build_query() は、配列をURL用のクエリ文字列に変換する関数です。

今回は、エラー配列と入力値をまとめて index.php に渡しています。

header('Location: ...') でフォーム画面へリダイレクトし、exit でその後の処理を止めます。

エラーがある
↓
index.phpへ戻す
↓
エラーメッセージを表示する
↓
入力値も戻す

style.cssを修正する

エラー表示用の見た目も追加します。

style.css を以下の内容にします。

:root {
  --color-bg: #f5f5f5;
  --color-text: #222222;
  --color-surface: #ffffff;
  --color-border: #cccccc;
  --color-muted: #666666;
  --color-error: #c0392b;
  --color-error-bg: #fff5f5;

  --space-s: 8px;
  --space-m: 16px;
  --space-l: 24px;
  --space-xl: 32px;
  --space-2xl: 64px;

  --radius-m: 8px;
  --radius-l: 16px;
  --radius-pill: 999px;

  --font-base: sans-serif;
}

body {
  margin: 0;
  font-family: var(--font-base);
  background-color: var(--color-bg);
  color: var(--color-text);
}

.contact-page {
  max-width: 640px;
  margin-inline: auto;
  padding-block: var(--space-2xl);
  padding-inline: var(--space-l);
}

.contact-page__title {
  margin-block-start: 0;
  margin-block-end: var(--space-m);
  font-size: 32px;
  text-align: center;
}

.contact-page__lead {
  margin-block-start: 0;
  margin-block-end: var(--space-xl);
  line-height: 1.8;
  text-align: center;
}

.contact-form,
.result-box,
.error-box {
  display: grid;
  gap: var(--space-l);
  padding-block: var(--space-xl);
  padding-inline: var(--space-l);
  border-radius: var(--radius-l);
  background-color: var(--color-surface);
}

.error-box {
  margin-block-end: var(--space-l);
  border-width: 1px;
  border-style: solid;
  border-color: var(--color-error);
  background-color: var(--color-error-bg);
  color: var(--color-error);
}

.error-box__title {
  margin-block-start: 0;
  margin-block-end: 0;
  font-weight: bold;
}

.error-box__list {
  margin-block-start: 0;
  margin-block-end: 0;
  padding-inline-start: var(--space-l);
}

.contact-form__field,
.result-box__item {
  display: grid;
  gap: var(--space-s);
}

.contact-form__label,
.result-box__label {
  font-weight: bold;
}

.contact-form__control {
  width: 100%;
  padding-block: 12px;
  padding-inline: 12px;
  border-width: 1px;
  border-style: solid;
  border-color: var(--color-border);
  border-radius: var(--radius-m);
  font-size: 16px;
  box-sizing: border-box;
}

.contact-form__textarea {
  min-height: 160px;
  resize: vertical;
}

.contact-form__button,
.result-box__link {
  display: inline-block;
  padding-block: 14px;
  padding-inline: var(--space-l);
  border-width: 0;
  border-radius: var(--radius-pill);
  background-color: var(--color-text);
  color: var(--color-surface);
  font-size: 16px;
  font-weight: bold;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}

.contact-form__button:hover,
.result-box__link:hover {
  opacity: 0.8;
}

.result-box__value {
  margin-block-start: 0;
  margin-block-end: 0;
  color: var(--color-muted);
  line-height: 1.8;
  white-space: pre-wrap;
}

今回追加した主なクラスは、error-box 系です。

  • error-box
  • error-box__title
  • error-box__list

エラーはユーザーがすぐ気づける必要があるため、色と枠で目立つようにしています。


実際に試してみる

Laragonで以下のURLを開きます。

http://contact-form-app.test/

まず、何も入力せずに送信してみます。

フォーム画面に戻り、以下のようなエラーが表示されれば成功です。

  • お名前を入力してください。
  • メールアドレスを入力してください。
  • お問い合わせ内容を入力してください。

次に、メールアドレス欄に test のような不正な形式を入れて送信してみます。

以下のようなエラーが表示されればOKです。

メールアドレスの形式で入力してください。

最後に、すべて正しく入力して送信します。

送信内容の確認画面が表示されれば成功です。


curlでもチェックしてみる

フォーム画面を通らず、Git Bashからcurlで直接POSTしても、PHP側チェックは効くはずです。

空データを送ってみます。

curl -i -X POST http://contact-form-app.test/receive.php \
  -d "name=" \
  -d "email=" \
  -d "message=" 

レスポンスを見ると、フォーム画面へリダイレクトするはずです。

次に、不正なメールアドレスを送ってみます。

curl -i -X POST http://contact-form-app.test/receive.php \
  -d "name=Leon" \
  -d "email=not-email" \
  -d "message=テストです" 

このように、ブラウザのJavaScriptを通らない送信でも、PHP側でチェックできることが大事です。


今回のコードはまだ完璧ではない

今回の方法では、エラーや入力値をURLパラメータで戻しています。

MVPとしては分かりやすいですが、実務では注意が必要です。

  • URLが長くなる
  • 入力内容がURLに見える
  • 長文の問い合わせ内容には向かない
  • 本格的にはセッションでエラーや入力値を扱うことが多い

ただし、今回は学習用として、処理の流れが見えやすいことを優先しています。

まずは、PHP側でチェックして、エラーがあれば戻すという流れを理解できれば十分です。


今回わかったこと

今回は、PHP側で入力チェックを行い、エラーがあればフォームに戻して表示しました。

学んだことは以下です。

  • サーバー側でも必ず入力チェックする
  • 空欄チェックはPHP側でも行う
  • filter_var()でメール形式をチェックできる
  • エラーは配列で管理できる
  • エラーがあればheader(‘Location: …’)で戻せる
  • http_build_query()でクエリ文字列を作れる
  • フォームに入力済みの値を戻すとUXが良くなる
  • 戻す値も必ずエスケープする
  • curlから直接POSTしてもPHP側チェックが効く

ここまで来ると、フォーム処理はかなり実務に近づいてきます。


次回予告

次回は、今回のフォーム処理をさらに改善します。

POST後にそのまま確認画面を表示すると、ブラウザの再読み込みで同じ送信が繰り返される問題があります。

次回のテーマは、以下です。

【PRGパターン入門】フォームの二重送信を防ぐ基本設計

フォームは送って終わりではありません。

送った後に、再読み込みや戻るボタンで何が起きるかまで考える必要があります。

次回は、二重送信の罠に入ります。