Menu

PHPのフォーム処理:送信、バリデーション、再表示

PHPのフォームは<form method="post">で自分自身に送信し、$_SERVER['REQUEST_METHOD']が'POST'になったら、スクリプトが$_POST['name']から項目を読みます。バリデーション、項目の隣のエラーメッセージ、入力値の保持、チェックボックスとセレクト、出力のエスケープを学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

PHPのフォームは、それを出力したスクリプトに送信されます。名前を付けた入力欄で<form method="post">を書き、$_SERVER['REQUEST_METHOD']が'POST'のときに、各項目を$_POST['name']として読みます。出力し直すものはすべてhtmlspecialchars()でエスケープしましょう。

このブロックを実行し、フォームに名前を入力してSendを押すと、今度は$_POSTに値が入った状態で同じスクリプトがもう一度実行されます。

ここでは、すべてのフォームの処理に必要な3つのことが起きています。

  • メソッドの確認。 最初の訪問ではリクエストはGETで、処理するものはありません。?? 'GET'が意味を持つのは、REQUEST_METHODが設定されないコマンドラインからファイルを実行したときだけで、Webサーバーでは常に設定されています。
  • すべての項目に?? ''。 項目は欠けていることがあり(名前を変えた入力欄、スクリプトから送られたリクエスト)、存在しないキーを読むと警告が出ます。
  • 出力時のエスケープ。 名前はページに2回戻され、htmlspecialchars()が<b>Ada</b>のような値をマークアップではなくテキストとして表示させます。詳しくはhtmlspecialcharsのページにあります。

フォームを検証し、項目の隣にエラーを表示する

エラーは項目名をキーにした配列に集めます。配列が空ならフォームは正しく、そうでなければ各メッセージを項目の下に置き、訪問者の入力を残したままフォームをもう一度出力します。空のまま送信し、次に不正なメールアドレスで、最後に正しい値で送信してみてください。

novalidateはブラウザ自身のチェックを無効にするので、PHPのチェックが動くのを確認できます。本物のフォームでは両方を残しましょう。ブラウザの検証は訪問者のための便利機能で、意味を持つのはPHPの検証です。誰でもあなたのフォームを使わずにリクエストを送れるからです。filter_var()はメールアドレスと整数の範囲をそれぞれ1回の呼び出しで確認します。ほかのフィルターはfilter_varのページで扱っています。

チェックボックス、ラジオボタン、セレクト

テキスト欄は、空であっても常に値を送ります。ほかのコントロールは違います。

  • チェックされていないチェックボックスは何も送らないので、そのキーは$_POSTにありません。
  • topics[]という名前のチェックボックスは配列で届きます:$_POST['topics']は['php', 'sql']です。
  • ラジオボタンのグループや<select>は、選ばれたオプションのvalueを送ります。

送信後も選択を保つには、一致するオプションにcheckedかselectedを出力します。いくつかのボックスにチェックを入れ、レベルを選んでSaveを押してください。

入力を読んだあとの2つの確認は、見た目以上に重要です。array_intersect()は一覧になかったトピックを取り除き、is_string()とarray_key_exists()の確認は、提示したことのない値(または$levels[$level]で例外を投げさせる配列)が送られてきたときにレベルを既定値に戻します。セレクトが制限するのはブラウザが送れるものであって、リクエストに含められるものではありません。

hiddenフィールドを使った複数ステップのフォーム

hiddenの入力欄は、値を表示せずに次のステップへ運びます。ここではステップ1で名前を尋ね、ステップ2で言語を尋ねながらhiddenフィールドで名前を一緒に送り、ステップ3で両方を表示します。3つのステップをすべて進んでみてください。

hiddenフィールドはページのソースで見え、ほかの項目と同じように編集できるので、訪問者自身の回答をステップ間で運ぶのには問題ありませんが、価格、ユーザーID、権限には決して使ってはいけません。それらはサーバーのセッションに置きましょう。セッションとCookieを参照してください。

Post/Redirect/Get:再読み込みでの再送信を止める

POSTが成功したあと、スクリプトが「ありがとう」のページを直接出力すると、再読み込みを押したときにブラウザがPOSTをもう一度送ろうとし、注文やコメントが2回保存されてしまいます。解決策は、保存してリダイレクトし、結果をGETで表示することです。

<?php
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $errors = validate($_POST);
    if (!$errors) {
        save_comment($_POST);
        $_SESSION['flash'] = 'Comment posted.';
        header('Location: /comments.php', true, 303);
        exit;
    }
    // On errors, fall through and print the form with the messages.
}

$flash = $_SESSION['flash'] ?? null;
unset($_SESSION['flash']);

303 See Otherは、ブラウザにGETでリダイレクトに従うよう伝えます。header()のあとのexitは重要で、それがなければスクリプトの残りが実行され続けます。リダイレクトは何かを出力する前に行う必要もあり、そうでなければPHPは「headers already sent」で拒否します。

本物のフォームにはCSRFトークンも加えます。セッションとhiddenフィールドに保存したランダムな値を、送信時にhash_equals()で比較するもので、ログイン中の訪問者になりすましてほかのサイトがあなたのフォームに送信するのを防ぎます。

ブラウザなしでバリデーションをテストする

ルールを、配列を受け取ってエラーを返す関数にまとめます。ページは$_POSTでそれを呼び、あなたは好きな配列でそれを呼べるので、境界のケースを手軽に試せます。一覧に自分のケースを加えてみてください。

4つ目のケースは忘れられがちです。リクエストはname[]=xを送れ、そうすると$_POST['name']が配列になり、配列をtrim()に渡すとTypeErrorが投げられてスクリプトが止まります。先にis_string()を確認すれば、悪意のある入力も普通のバリデーションエラーになります。前のブロックの(string)キャストもTypeErrorを避けますが、配列をArrayという単語に変えて警告を出すので、is_string()のほうがすっきりした確認です。

よくあるミス:フォームが送信される前に$_POSTを読む

最初のフォームのスクリプトでいちばんよくあるのは、ファイルの先頭で項目を読むことです。

<?php
$name = $_POST['name'];
echo "Hello, $name";

最初の訪問では何も送信されていないので、PHPは次のように表示します。

Warning: Undefined array key "name" in /var/www/form.php on line 2

解決策は最初のブロックのパターンです。リクエストがPOSTのときだけフォームを処理し、すべての項目を既定値付きで読みます:$_POST['name'] ?? ''。送信後に警告が出るなら、HTMLのname属性とPHPのキーを比べてください。name="Name"と$_POST['name']は別のキーです。

よくある質問

PHPのフォームを同じページに送信するには?

actionなし(またはaction="")で<form method="post">と書きます。ブラウザは現在のURLに送信し直すので、同じスクリプトがフォームを表示し、処理もします。$_SERVER['REQUEST_METHOD'] === 'POST'を確認し、$_POSTから項目を読みます。

PHPで送信後もフォームの値を保持するには?

送信された値をエスケープして項目に出力し直します:<input name="email" value="<?= htmlspecialchars($_POST['email'] ?? '') ?>">。チェックボックスならcheckedを、セレクトのオプションならselectedを、送信された値が一致したときに出力します。

PHPで複数のチェックボックスの値を取得するには?

チェックボックスに角括弧で終わる同じ名前を付けます:name="topics[]"。するとPHPはチェックされた値を配列$_POST['topics']に入れます。チェックされていないボックスは何も送らないので、$_POST['topics'] ?? []を使い、すべての値を提示した一覧と照合します。

ページを再読み込みするとPHPのフォームが再送信されるのはなぜですか?

最後のリクエストがPOSTだったので、ブラウザが再読み込みでそれを繰り返すからです。送信が成功したら、header('Location: /thanks.php'); exit;でリダイレクトします(Post/Redirect/Getパターン)。そうすれば再読み込みが繰り返すのは無害なGETになります。

PHPの$_POSTはそのまま使っても安全ですか?

安全ではありません。$_POSTのすべての値は訪問者から来たもので、HTML、SQL、文字列を期待したところに配列など、何でもありえます。受け取るときに検証し(filter_var、長さや許可リストのチェック)、出力するときにエスケープします(HTMLにはhtmlspecialchars、SQLにはプリペアドステートメント)。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める