PHP Logo フォーム

PHP を使ったフォームの操作(フォーム処理や input 要素、ボタン等のフォーム部品の使い方)や検証、エスケープ処理等に関する参考資料です。簡単なアンケートフォームのサンプルも掲載しています。

更新日:2024年03月25日

作成日:2016年01月24日

関連ページ

フォームの利用

フォームはユーザーが情報を入力することを可能にする要素で、「テキストフィールド」「ラジオボタン」「チェックボックス」「テキストエリア」「サブミットボタン」などの部品があります。

これらのフォームの要素にユーザーが入力した内容は、「サブミットボタン」をクリックすることによりサーバーに送信されます。

HTML にはフォームを表示する form タグがあり、<form> で始まり、</form> で終わります。フォーム要素の詳細は「フォームの設置」を参照ください。

フォームを構成する各部品は、form タグの中に配置します。

<form action="sample.php" method="post" id="form_sample1">
フォームを構成する各部品
</form>

以下は、1行テキスト入力フィールド(type 属性が text の input 要素)のフォーム部品と送信(submit)ボタンのみの単純なフォームの例です。

<form action="form_sample1.php" method="post" >
<p> 名前: <input type="text" name="your_name" value=""></p>
 <input type="submit" >
</form> 

このフォームのテキストフィールドにユーザーが値(文字)を入力して送信ボタンをクリックすると、以下のような流れになります。

  • form 要素の action 属性に指定されたプログラム(form_sample1.php)に、method 属性で指定された方法(上記の場合は POST メソッド)でデータ(値)が送信されます。
  • PHP は送信されてきたデータ(値)を、フォーム部品の name 属性の値をキーとした連想配列 $_POST['your_name'] に自動的に格納します。
  • action 属性に指定されたプログラム(form_sample1.php)では、$_POST['your_name'] を使って値を処理することができます。

フォームに関連付けられるプログラムは、原則として1つだけです。1つのフォームの中で送信ボタンを複数設置するような使い方はできません。そのような場合は、フォームを分けて設計します。

また、form タグは1ページに複数設置することができます。

フォームの概要

データの送信

データを送信するには、「どこに」「どのような方法で」送るのかを記述する必要があり、そのための次のような属性が form タグにあります。

  • action 属性:「どこに」→データを送信する URI(プログラムファイル)を指定
  • method 属性:「どのような方法で」→POST または GET のいずれかを指定

form タグの action 属性

action 属性は、どこにデータを送信するかを定義します。このため、action 属性にはフォームの内容を処理するプログラムの URL(相対 URL または絶対 URL)を指定します。

<form action="confirm.php" method="post" id="contact">

<form> タグの action 属性が「""」(空)である場合は、自分自身(現在のファイル)に送信することを意味します。但し、HTML5 からは自分自身に送信する場合は action 属性自体を省略します。

※HTML5 からは action 属性を省略することができ、省略した場合はフォームが表示されているページ自身に対してデータを送信します。

<form id="my_form" method="post">

HTML5 で action="" を指定すると、W3Cの構文チェック(The W3C Markup Validation Service)では「Error: Bad value for attribute action on element form: Must be non-empty.(form 要素の action 属性の値は空であってはならない)」のようなエラーになります。

$_SERVER['PHP_SELF'] も自分自身への送信になりますが、$_SERVER['PHP_SELF'] を action 属性値として直接 <form> タグに記述すると XSS 脆弱性となるので、絶対に避けるべきです。「""」(空)にするか(HTML5 の場合は action 属性自体を省略)、以下のように htmlspecialchars() 関数でエスケープします。

<form method="post" action="<?php echo htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8'); ?>"> 

method 属性:POST と GET

method 属性には、POST または GET のいずれかを指定します。以下のような違いがあります。

  • POST リクエストは、フォームの入力データをメッセージボディに格納して送信します。
  • GET リクエストは、フォームの入力データを URL の一部として送信します。

method 属性を指定しない場合は、デフォルトの GET リクエストになります。

詳細は「HTTP 通信」を参照ください。

フォームからのデータ受信

PHP では、フォームから送信されたデータは自動的に特別な連想配列(スーパーグローバル変数)に格納され、スクリプト全体を通してすべてのスコープで使用することができます。

  • POST メソッドで送信されたデータは、$_POST という連想配列に格納されます。
  • GET メソッドで送信されたデータは、$_GET という連想配列に格納されます。

例えば、フォーム部品の name 属性が「foo」の場合、POST メソッドで送信されたデータは $_POST['foo'] という連想配列に自動的に格納されます。

$_POST と $_GET の他に $_REQUEST という連想配列もあり、$_REQUEST には POST メソッドのデータも GET メソッドのデータも両方格納されます。

但し、同じスクリプト内でどちらの形式も使用するような場合以外は、それぞれのメソッドに応じた $_POST や $_GET を使用したほうが良いでしょう。

$_REQUEST には、GET, POST, COOKIE, FILE データの混ざったものが含まれます。

簡単なフォームの例/POST メソッド

<form action="form_sample1.php" method="post" >
<p> 名前: <input type="text" name="name" value=""></p>
<p>年齢: <input type="text" name="age" value=""></p>
 <input type="submit" >
</form> 

名前:

年齢:

ユーザーがこのフォームを記入し、送信ボタンをクリックすると、 form タグの action 属性に指定した form_sample1.php が呼び出されます。

form_sample1.php

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>フォームサンプル1</title>
</head>
<body>
<p><?php echo htmlspecialchars(@$_POST['name'], ENT_QUOTES, 'UTF-8'); ?>さん。</p>
<p>あなたは、<?php echo (int)@$_POST['age']; ?> 歳です。</p>
</body>
</html>

PHP の部分のみを抜き出すと以下になります。

<?php echo htmlspecialchars(@$_POST['name'], ENT_QUOTES, 'UTF-8'); ?>
<?php echo (int)@$_POST['age']; ?> 

フォーム部品の name 属性

上記の例の場合 form タグの method 属性は、post を指定しているので、送信されたデータは $_POST という連想配列に格納されます。

連想配列のキーは、フォーム部品の name 属性を指定します。

<input type="text" name="name" value="">
<input type="text" name="age" value="">

上記のフォーム部品(input 要素/1行テキスト入力フィールド)の name 属性は「name」と「age」なので、以下のようにして送信されたデータを取得できます。

$_POST['name']  //POST メソッドで name="name"に入力されて送信された値
$_POST['age']  //POST メソッドで name="age"に入力されて送信された値

フォーム部品の value 属性

value 属性は、フォーム部品の種類によって値の意味が異なりますが、初期値を設定しておくことが可能です。詳細は「フォームの設置(value 属性)」を参照ください。

以下のように、value 属性に値を設定すると初期値とすることができます。

<input type="text" name="name" value="Your name">

名前:

value 属性に初期値を設定した場合に、ユーザーが何も入力しないでフォームを送信すると初期値がそのフォーム部品の値として送信されますが、ユーザーが値を入力するとその値が送信されます。

@ エラー制御演算子

最初にページにアクセスした場合、フォームから POST されていない(送信されていない)ので、$_POST['name'] や $_POST['age'] はまだ存在していません(定義されていません)。

そのため、上記の例の場合、@ エラー制御演算子で Notice エラー(Notice: Undefined index:)が表示されないようにしています。GET でも同様です。

通常は isset() を使って、変数が定義されているかを調べて条件分岐したり、filter_input() を使うのが良いかと思います。

filter_input()

または、以下のように filter_input() を使うと簡潔に記述できます。

$name = filter_input(INPUT_POST, 'name');

上記は以下と同じことです。

if(isset($_POST['name'])){
  $name = $_POST['name'];
} else {
  $name = null;
}
// または
$name = isset($_POST['name']) ? $_POST['name'] : null;

filter_input() は受け取った値をオプションでそれをフィルタリング(検証やサニタイズ)することもできます(フィルタの型)。

例えば以下は、$_GET['email'] で取得した値が正しい email の形式であれば、変数 $email にその値を、正しくない形式の場合は false(値が設定されていない場合は null)が代入されます。

$email = filter_input(INPUT_GET, 'email', FILTER_VALIDATE_EMAIL);
if($email) {
  echo $email;  //正しい email の形式の場合のみ出力される
}

以下は $_GET['string'] で取得した値をサニタイズ(この場合は '"<>& を HTML エンコード)する例です。

例えば、$_GET['string'] で取得した値が <p>test</p> の場合は、&#60;p&#62;test&#60;/p&#62; が echo で出力されます。

$string = filter_input(INPUT_GET, 'string', FILTER_SANITIZE_SPECIAL_CHARS);
if($string) {
  echo $string;
}

エスケープ処理

セキュリティ上の理由からフォームなどから入力されたユーザーのデータをブラウザに表示する際は、原則すべてのデータを出力時に htmlspecialchars() 関数を使用してエスケープ(サニタイズ)処理(HTMLの特殊文字を無効化)します。

この処理を行わないと、XSS(クロスサイトスクリプティング)の脆弱性が発生するので、必ず行うようにします。

また、悪意のある攻撃者はブラウザを使わずにサーバーにデータを送信することができるので、ユーザーからの入力値はサーバーで受け取った時に必ず検証する必要があります。

前述の例(form_sample1.php)での、ユーザーの入力データの出力(表示)は以下のようになっています。

<?php echo htmlspecialchars(@$_POST['name'], ENT_QUOTES, 'UTF-8'); ?>

htmlspecialchars()

htmlspecialchars() 関数は、特殊文字を HTML エンティティ(文字参照)に変換して特殊文字を無効化します。

以下が htmlspecialchars() 関数の構文です。

string htmlspecialchars( $string ,$flags ,$encoding )

以下がパラメータです。(実際には、この他にもパラメータやオプションがあります)

  • string(string):変換される文字列(PHP 8.1.x から null を渡すのは非推奨)。
  • flags(int):ENT_QUOTES(*必ずこれを指定する):シングルクオートとダブルクオートを共に変換します。(フラグは他にもあります)
  • encoding(string):文字を変換するときに使うエンコーディングを定義します。 (オプションですが、必ず指定します)

この関数は、関数名も長く、パラメータも多いので頻繁に使用する場合は、以下のようにユーザー定義関数にしておくと便利です。

<?php
//h() 関数の定義
function h($str) {
  if($str === null) return '';  // PHP 8.1.x から null を渡すのは非推奨
  return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?>

PHP 8.1.x から第1引数に null を渡すと Deprecated エラーになるので、上記では引数が null の場合は、htmlspecialchars() を呼び出さずに空文字列にを返しています。

このようにユーザー定義関数 h() を定義しておけば、前述の記述は以下のように簡単になります。

<?php echo h(@$_POST['name']); ?>

また、前述の例(form_sample1.php)のもう1つの出力は以下のようになっています。

<?php echo (int)@$_POST['age']; ?>

age フィールドには数値が入ることがわかっているので、これを integer 型に変換(型キャスト)します。これにより、おかしな文字が入力されることを防ぎます。

参考:「フォームの処理

クロスサイトスクリプティング

クロスサイトスクリプティング(XSS)とは、サイト間を横断して攻撃用の JavaScript コードを送る攻撃手法から命名されたものですが、セキュリティ上の不備を利用して悪意のあるスクリプト(JavaScript など)を注入する攻撃全般を指します。

以下は JavaScript コードの中に XSS 脆弱性がある例です。

<script>
document.write("URL:" + document.location.href);
</script>

document.location.href には現在の URL が設定されますが、URL の後に「?」や「#」に続けて任意の文字列を指定できます。上記のコードは、document.location.href の値を document.write でそのまま出力しているため、攻撃者がそこに攻撃スクリプトを注入することができ、ブラウザによっては XSS 攻撃が成功してしまいます。

XSS 対策

外部からの入力文字列をそのままページに出力した場合、XSS 脆弱性のあるコードになります。外部からの入力を出力する場合は、必ず適切にエスケープ処理することが対策になります。但し、ユーザーからの入力値かどうかを判断するのは難しい場合もあるので、基本的に「表示する全ての変数をエスケープ処理する」ようにすると良いでしょう。

JavaScipt が実行されないように、htmlspecialchars() 関数を使って HTML 内の特別な意味を持つ文字「<」「>」「&」「"」「'」を文字参照に変換します。

文字参照に変換されれば、HTML タグなどの特別な意味はなくなり、そのまま文字列として表示されます。

注意点としては、htmlspecialchars() 関数の第2パラメータに必ず「ENT_QUOTES」を指定し、第3パラメータのエンコーディングも必ず指定するようにします。

変数を表示する場合は、デフォルトで htmlspecialchars() 関数を使ってエスケープ処理して出力します。

また、外部からの値をどこに書き出すかによっては、htmlspecialchars() 関数だけでは、XSS を防げない場合があるので注意が必要です。

例えば以下のように記述してしまうと htmlspecialchars() 関数では XSS を防ぐことができなません。

//このようなコードは書いてはいけません
<a href="<?php echo $val; ?>">
//このようなコードは書いてはいけません
<img src="<?php echo $input; ?>">  

URL を指定する場所の場合、「javascript:」で始めることにより、JavaScript を記述できるので、htmlspecialchars() 関数では JavaScript の記述を防ぐことができません。以下の場合、JavaScript が実行されてしまいます。

//htmlspecialchars() 関数では JavaScript の記述を防ぐことができません。
<a href="<?php echo htmlspecialchars("javascript:alert('Dangerous!')"); ?>">click</a>   

このような場所には、外部からの入力を表示しないようにします。また、入力値の検証を行わないで属性値にユーザーの入力値を出力しないことが大切です。

また、以下のような場所はエスケープ処理ができないのでこのようなコードは書いてはいけません。

//このようなコードは書いてはいけません
<script><?php echo $val; ?></script>
//このようなコードは書いてはいけません
<style><?php echo $val; ?></style>

以下の値もそのままページに表示(出力)せずに、必ずエスケープ処理をして表示する必要があります。

  • $_SERVER['HTTP_USER_AGENT']
  • $_SERVER['HTTP_REFFERER']
  • $_SERVER['PHP_SELF']

また、不完全なマルチバイト文字列による攻撃などもあるので、検証の際に文字エンコーディングのチェックなども行うようにする必要があります。

HTML タグを許可したい場合

ユーザーに HTML タグの入力を許可したい場合は、htmlspecialchars() 関数では対応できません。strip_tags() 関数というタグを取り除く関数がありますが、第2パラメータに許可するタグを指定すると、そのタグの属性値もそのまま残ってしまうため、攻撃が可能になり脆弱性があります。

一部のタグを許可したい場合などは、そのような処理を安全に行ってくれる HTML Purifier などのライブラリを使うのが無難です。

GET メソッドでのデータ送信

GET リクエストは、フォームの入力データを URL の一部として送信します。

以下は、method 属性に GET リクエストを指定したフォームの例です。

また、<form> タグの action 属性を「""」(空)に指定して、自分自身に送信するようにしているので、送信されたデータを取得する PHP の処理も記述してあります。

前述の例では、@ エラー制御演算子で最初にページにアクセスした場合に Notice エラーが表示されるのを回避していましたが、この例では、isset() を使ってまだ変数が定義されていない場合は、空文字を指定してあります。

<?php
//h() 関数の定義
function h($str) {
  if($str === null) return '';
  return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
if(isset($_GET['name'])) {
  //$_GET['name']が定義済みの場合は、値をエスケープ処理して $name に代入
  $name = h($_GET['name']);
}else{
  //$_GET['name']が未定義の場合は、$name に空文字を代入
  $name = "";
}
if(isset($_GET['age'])) {
  $age = (int)$_GET['age'];
}else{
  $age = "";
}
?>
<form action="" method="get">
<p> 名前: <input type="text" name="name" ></p>
<p>年齢: <input type="text" name="age" ></p>
  <input type="submit" >
</form>
<p><?php echo $name; ?>さん。</p>
<p>あなたは、<?php echo $age; ?> 歳です。</p>

以下のフォームの「名前」に「マイケル」、「年齢」に「33」を入力して、Submit ボタンをクリックしてアドレスバーを確認すると、URL の最後に「?name=マイケル&age=33」または「?name=%E3%83%9E%E3%82%A4%E3%82%B1%E3%83%AB&age=33」(URLエンコード結果)という文字列が追加されているのがわかると思います。

※ &scroll_top=5127 というような文字列も追加されていますが、これは無視してください(後述)。

GET 要求ではこのようにURLに「クエリ」という形でデータを付けて送信されます。「クエリ」文字列の付けかたは、「?」の後に「name=value」の形式で渡します。値が複数ある場合は「&」でつなげます。

またその際に、値に日本語などのマルチバイト文字や特殊文字が含まれていると、URLエンコードと呼ばれる変換処理によって URL として認められた文字に変換されます。(最近のブラウザでは、マルチバイト文字でもそのまま表示されます)

名前:

年齢:

さん。

あなたは、 歳です。

この機能を利用して、フォーム以外(リンク)からデータを送信することが可能です。

例えば、http://www.example.com/sample.php というファイル(送信先)に GET リクエストで「?name=michael&age=33」というクエリを送信するには、いかのようなリンクをクリックすることでデータが送信されます。

<a href="http://www.example.com/sample.php?name=michael&age=33" >データを送信</a>

送信されたデータは、フォームで送信した場合と同様に以下のような記述で取得できます。

$_GET['name']  //michael
$_GET['age']  //33
URLエンコード

フォームを使用する場合は、値は自動的に URL エンコードされますが、リンクから PHP に値を渡す場合は、マルチバイト文字などを自分で URL エンコードする必要があります。

URL エンコードするには urlencode() 関数を使います。以下が urlencode() 関数の書式です。

string urlencode( $str )

パラメータ

  • str(string):エンコードする文字列。

戻り値

「-」「_」「.」 を除くすべての非英数文字が % 記号 (%)に続く二桁の数字で置き換えられ、 空白は + 記号(+)にエンコードされます。

以下は、「name=マイケル」という値を「sample.php」に送信する場合のリンクの例です。

<a href="sample.php?name=<?= urlencode("マイケル"); ?>">データを送信</a>

以下は、「?name=マイケル&age=33」という値を自分自身($_SERVER['PHP_SELF'] )に送信するリンクの例です。

<?php
$name = urlencode('マイケル');
$link = dirname(htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8')) . '/'.basename(__FILE__) .'?name='.$name.'&age=33';
echo '<a href="' .$link .'">データを送信</a>';
?>

データを送信

value 属性に変数(値)を設定

受け取った値を任意の変数に代入し、その変数を value 属性に設定することができます。

そうすることで再度入力しなおす場合などに、入力した値を残すことができます。

<?php
//h() 関数(エスケープ処理のユーザ定義関数)の定義
function h($str) {
  if($str === null) return '';
  return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
if(isset($_GET['text_input'])) {
  //$_GET['text_input']がすでに定義されている(値が送信されている)場合
  $text_input = h($_GET['text_input']);
}else{
  //値がまだ送信されていない(定義されていない)場合
  $text_input = "";
}
?>
<form action="" method="get" class="form_sample">
<p> テキスト: <input type="text" name="text_input" value="<?php echo $text_input ?>" ></p>
 <input type="submit" >
</form>
<p>入力テキスト:<?php echo $text_input; ?></p>

15行目の value 属性のように、変数($text_input)を出力(echo)することで値を設定できます。

<input type="text" name="text_input" value="<?php echo $text_input ?>" >

echo の短縮形を使って以下のように記述することもできます。

value="<?= $text_input ?>"
//以下と同じことです。
value="<?php echo $text_input ?>"

テキスト:

入力テキスト:

submitした時の位置を保持

フォームを送信(submit)すると、 action 属性で指定したファイルが読み込まれ、送信した際の画面の表示位置はリセットされページの先頭が表示されます。

フォームがページの中ほどや後半にある場合で、自分自身($_SERVER['PHP_SELF'])に送信した際に、送信(submit)した際の表示位置を保持して、ページロード後もスクロール位置を変更しないで表示したいことがあります。

そのためには、Javascript(jQuery)で submit した時の位置情報を取得して、ロード後の画面でその位置に戻すようにします。

以下が概要です。

  • フォームにその位置の情報を保持するための隠しフィールド(type = "hidden")を用意
  • 送信する際に、Javascript(jQuery)で submit した時の位置情報を取得
  • 取得した位置情報を隠しフィールドの val 属性に設定
  • ページがロードされたら、隠しフィールドの val 属性に設定された位置情報を取得
  • Javascript(jQuery)で位置を調整

まずは、フォームにその位置の情報を保持するための隠しフィールド(type = "hidden")を用意します。以下の例では、「name="scroll_top"」、「class="st"」としています。

<form action="" method="get" class="form_sample">
<p> 名前: <input type="text" name="name" ></p>
<p>年齢: <input type="text" name="age" ></p>
  <input type="hidden" name="scroll_top" value="" class="st">
  <input type="submit" >
</form>

以下のような Javascript(jQuery)を記述します。

  • フォームの submit イベント($('form').submit())を利用します。
  • $(window).scrollTop() で送信された際の位置を取得して変数「scroll_top」に代入します。
  • prop() を使って、隠しフィールドの「value」属性に位置の情報をセットします。
  • $('input.st',this) の this は、この例の場合、$('form') の form になります。(コンテクストの制御
  • window.onload を使ってロードされた際に、隠しフィールドにセットされた値を取得して、スクロール位置を設定します。
  • GET メソッドと POST メソッドの両方の場合に対応するように、値の取得は「$_REQUEST」スーパーグローバル変数を使用しています。
  • また、最初は、$_REQUEST['scroll_top']の値は定義されていないので、@ エラー制御演算子を使用しています。
$('form').submit(function(){
  var scroll_top = $(window).scrollTop();  //送信時の位置情報を取得
  $('input.st',this).prop('value',scroll_top);  //隠しフィールドに位置情報を設定
});

window.onload = function(){
  //ロード時に隠しフィールドから取得した値で位置をスクロール
  $(window).scrollTop(<?php echo @$_REQUEST['scroll_top']; ?>);
}

前述の「GET メソッドでのデータ送信」の例で、URL に「&scroll_top=5127 」というようなクエリが付いていたのはこのためです。

関連ページ:JavaScript フォームとフォームコントロールの使い方/送信時の位置を保持

フォーム部品

以下では、それぞれのフォーム部品について見ていきます。

テキストボックス

テキストボックスは、1行の文字列の入力をする際に使用します。1行(入力)テキストフィールドと呼ぶこともあります。

テキストボックスは、input 要素の type 属性に「text」を指定します。

<input type="text" name="sample" size="20" maxlength="20" value="初期値">
  • type 属性:text を指定
  • name 属性:フォーム部品を識別するための名前を指定(連想配列のキーとなる)
  • value 属性:値を指定すると、初期値として表示されます
  • size 属性:入力欄の長さ(幅)を指定
  • maxlength 属性:最大文字数を指定

maxlength 属性

maxlength 属性を指定すると、ブラウザで入力する文字数を制限することができるので、サーバーに最大文字数を超える文字列が送られることはありませんが、これはセキュリティのための機能ではありません。

悪意のある攻撃者はブラウザを使わずにサーバーにデータを送信することができるので、最大文字数を超える文字列をサーバーに送信できてしまいます。そのためユーザーからの入力値はサーバーで受け取った時に必ず検証する必要があります。

以下はテキストボックスのサンプルです。

<?php
if(isset($_POST['text_sample'])) {
  $text = htmlspecialchars($_POST['text_sample'], ENT_QUOTES, 'UTF-8');
}else{
  $text = "初期値";
}
?>

<form method="post" action="" class="form_sample">
<p> テキスト:
  <input type="text" name="text_sample" size="40" value="<?= $text ?>">
</p>
<input type="submit" value="送信">
</form>
<?php echo '<p>送信された値:'. $text . '</p>'; ?>

form の action 属性が「""」なので、自分自身(現在のファイル)に送信します。つまり、フォームも PHP の処理も同じファイルに記述されています。

form の method 属性が「post」なので送信されたデータは、$_POST という連想配列(スーパーグローバル変数)に格納されます。

テキストボックスの name 属性が「text_sample」なのでスーパーグローバル変数のキーに「text_sample」を指定すると($_POST['text_sample'] )入力された値を取得することができます。

テキストボックスの value 属性に受け取った値を変数「$text」に代入し、echo の短縮(省略)形で設定しています。これにより、再度入力しなおす場合などに、入力した値を残すことができます。

  • 2行目:isset() を使って $_POST['text_sample'] がすでに定義されているかを調べます。
  • 3行目:すでに定義されている場合は、送信された値を htmlspecialchars() でエスケープ処理して変数「$text」に代入しています。
  • 5行目:$_POST['text_sample'] がまだ定義されていない場合は初期値を変数「$text」に代入しています。
  • 15行目:echo で値を出力しています。
送信ボタン

送信ボタン(サブミットボタン)はフォームに入力されたデータを action 属性で指定した送信先に送るときに使用します。

送信ボタンは、input 要素の type 属性に「submit」を指定します。

<input type="submit" name="send" value="送信ボタン">
  • type 属性:submit を指定
  • name 属性:フォーム部品を識別するための名前を指定(連想配列のキーとなる)
  • value 属性:値を指定すると、ボタンの名前としてその値を表示します

以下は、送信ボタンの name 属性に「send」を指定して、送信ボタンがクリックされたかを調べる例です。これにより、送信ボタンがクリックされて「post_03.php」に移動した場合と「post_03.php」を直接開いた場合の処理を分岐することができます。

<?php
  if (isset($_POST['send'])) {
    //送信ボタンがクリックされた場合
    echo "送信ボタンがクリックされました。";
  } else {
    //送信ボタンがクリックされていない(ファイルを直接開いた)場合
    echo "送信ボタンはクリックされていません。";
  }
?>
<form action="post_03.php" method="post">
<input type="submit" name="send" value="送信ボタン">
</form>

form の method 属性が「post」なので送信されたデータは、$_POST という連想配列(スーパーグローバル変数)に格納されます。

送信ボタンの name 属性が「send」なのでスーパーグローバル変数 $_POST のキーに「send」を指定して $_POST['send'] とします。

isset($_POST['send']) が true の場合、送信ボタンがクリックされてデータが送信され、 $_POST['send']が定義済みになっています。

パスワード入力ボックス

パスワード入力ボックスは入力したテキストが●やアスタリスク(*)などに置き換えて表示されます。

パスワード入力ボックスは、input 要素の type 属性に「password」を指定します。

<input type="submit" name="send" value="送信ボタン">
  • type 属性:text を指定
  • name 属性:フォーム部品を識別するための名前を指定(連想配列のキーとなる)
  • value 属性:通常パスワード形式の入力欄では初期値(value 属性の値)の設定は行わないようにします。(ソースを見ると初期値パスワードの文字列を確認できてしまいます)
  • size 属性:入力欄の長さ(幅)を指定
  • maxlength 属性:最大文字数を指定

maxlength 属性

maxlength 属性を指定すると、ブラウザで入力する文字数を制限することができるので、サーバーに最大文字数を超える文字列が送られることはありませんが、これはセキュリティのための機能ではありません。

悪意のある攻撃者はブラウザを使わずにサーバーにデータを送信することができるので、最大文字数を超える文字列をサーバーに送信できてしまいます。そのためユーザーからの入力値はサーバーで受け取った時に必ず検証する必要があります。

以下は送信されたパスワードが一致しているかを調べる例です。但し、通常はパスワードの長さや含まれる文字の種類等を検証するなどの処理が必要と思いますが、この例ではセキュリティに関しては考慮していません。

<?php
$pw = "password";
if(isset($_POST['pwd'])) {
  //$_POST['pwd']がすでに定義されている(値が送信されている)場合
  $input_pw = htmlspecialchars($_POST['pwd'], ENT_QUOTES, 'UTF-8');
  if($_POST['pwd'] === $pw) {
    echo "<p>パスワードが一致しました。<p>";
  }else{
    echo "<p>パスワードが一致しませんでした。<p>";
  }
}
?>

<form method="post" action="">
<p> パスワード: <input type="password" name="pwd" size="20"></p>
<input type="submit" value="確認">
</form>
隠しフィールド

隠しフィールドは、画面上に表示させず(フォーム部品として表示させないで)にプログラムにデータを送信する時に使用します。(ただし、ソースを見れば確認することができます)

隠しフィールドは、input 要素の type 属性に「hidden」を指定します。

  • type 属性:text を指定
  • name 属性:フォーム部品を識別するための名前を指定(連想配列のキーとなる)
  • value 属性:値を指定します。(値を指定しない意味がありません)
  • size 属性:入力欄の長さ(幅)を指定
  • maxlength 属性:最大文字数を指定

以下はサンプルです。

隠しフィールドは表示されませんが、送信ボタンをクリックすると隠しフィールドの値が送られます。

<?php
if(isset($_POST['hidden_data'])) {
  //$_POST['hidden_data']がすでに定義されている(値が送信されている)場合
  $hidden_data = htmlspecialchars($_POST['hidden_data'], ENT_QUOTES, 'UTF-8');
}
?>

<form method="post" action="">
<input type="hidden" name="hidden_data" size="40" value="隠しフィールドの値です。">
<input type="submit" value="送信">
</form>

<?php
echo '<p>送信された値:'. @$hidden_data .'</p>';
?>

最初にページにアクセスした場合、フォームから POST されていない(送信されていない)ので、$_POST['hidden_data'] はまだ存在していません(定義されていません)。そのため、14行目では、@ エラー制御演算子で Notice エラー(Notice: Undefined index:)が表示されないようにしています。GET でも同様です。

チェックボックス

チェックボックスは複数選択が可能で、checked 属性を指定すると、その項目についてはあらかじめチェックの付いた状態となります。

1つだけを選択させたい場合は、ラジオボタンを使用します。

value 属性の値は、そのチェックボックスが選択されている時に送信される値になります。

複数の input 要素で同じ名前(name 属性)を共有することができ、同時に複数の項目を選択することができます。

複数のチェックボックス

複数のチェックボックスを使う場合は、以下の2つの方法があります。

  • それぞれの name 属性の値を異なるものにして、name 属性と value 属性の値の1対1のペアとして送信する。
  • name 属性の値に [] を付けて、配列としてデータを送信する。

以下は、それぞれの name 属性の値を異なるものにする例です。

<?php
echo "選択された値 <br>";
if(isset($_POST['drink1'])) {
  //$_POST['drink1']がすでに定義されている(値が送信されている)場合
  $drink1 = htmlspecialchars($_POST['drink1'], ENT_QUOTES, 'UTF-8');
  echo $drink1. "<br>";
}
if(isset($_POST['drink2'])) {
  //$_POST['drink2']がすでに定義されている(値が送信されている)場合
  $drink2 = htmlspecialchars($_POST['drink2'], ENT_QUOTES, 'UTF-8');
  echo $drink2. "<br>";
}
?>

<form method="post" action="" class="form_sample">
<input type="checkbox" name="drink1" value="wine"> Wine
<input type="checkbox" name="drink2" value="beer"> Beer<br>
<input type="submit" value="送信">
</form>

以下は、複数の input 要素で同じ名前(name 属性)を使い、name 属性の値に [] を付けて、配列としてデータを送信する例です。

以下の例の場合、$_POST['drink'][0]から順番に選択されたチェックボックスの value 属性の値が表示されます。キーとの関係がわかりやすいように、キーも出力しています。

<?php
echo "選択された値 <br>";
if(isset($_POST['drink'])) {
  //$_POST['drink']がすでに定義されている(値が送信されている)場合
  foreach($_POST['drink'] as  $key => $value) {
    echo $key . ": " .htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . "<br>";
  }
}
?>

<form method="post" action="">
<input type="checkbox" name="drink[]" value="wine"> Wine
<input type="checkbox" name="drink[]" value="beer"> Beer
<input type="checkbox" name="drink[]" value="sake"> Sake<br>
<input type="submit" value="送信">
</form>

value 属性を数値(整数)に限定

value 属性に値そのものではなく、数値を指定にすることにより、入力検証をする際に「数値」であることをチェックすることによりセキュリティが向上します。

以下は、value 属性を数値(整数)に限定して出力の前に値を検証する例です。

数値(整数)かどうかを判定するには ctype_digit() 関数を使用します。

<?php
echo "選択された値 <br>";
if(isset($_POST['drinks'])) {
  //$_POST['drinks']がすでに定義されている(値が送信されている)場合
  $drinks = $_POST['drinks'];
  if(is_array($drinks)){  //配列かどうかのチェック
    foreach($drinks as  $value){
      if(ctype_digit($value)){  //入力値(配列の要素)は数字のみかを検証
        if($value >= 1 && $value <= 3){  //値の妥当性の検証(1以上3以下)
          switch($value){
            case 1:
              echo 'Wine<br>';
              break;
            case 2:
              echo 'Beer<br>';
              break;
            case 3:
              echo 'Sake<br>';
              break;
          }
        }
      }
    }
  }
}
?>

<form method="post" action="" class="form_sample">
<input type="checkbox" name="drinks[]" value="1"> Wine
<input type="checkbox" name="drinks[]" value="2"> Beer
<input type="checkbox" name="drinks[]" value="3"> Sake<br>
<input type="submit" value="送信">
</form>

PHP でチェックボックスを生成

チェックボックスの項目が多い場合、以下のように PHP でチェックボックスを生成すると簡単です。

<?php
$drink = array(1 => 'Wine', 2 => 'Beer', 3 => 'Sake');
foreach($drink as $key => $value) {
  echo '<label><input type="checkbox" name="drink[]" value="'.
  $key . '" />'  . $value . '</label>' . "\n";
}
?>

上記の PHP は以下のチェックボックスを生成します。

<label><input type="checkbox" name="drink[]" value="1"> Wine </label>
<label><input type="checkbox" name="drink[]" value="2"> Beer</label>
<label><input type="checkbox" name="drink[]" value="3"> Sake</label>
ラジオボタン

ラジオボタンは、複数の項目から1つを選ぶ際に使用します。

複数の中から一つしか選択できない点がチェックボックスと異なります。

複数を一つのグループとして認識させるには、name 属性で同じ名前を付けます。そして、それぞれに異なる value 属性を与えます。

checked 属性を指定すると、その項目についてはあらかじめチェックの付いた状態となります。

以下は、選択された値を表示する例です。

<?php
echo "選択された値 :";
if(isset($_POST['gender'])) {
  //$_POST['gender']がすでに定義されている(値が送信されている)場合
  echo htmlspecialchars($_POST['gender'], ENT_QUOTES, 'UTF-8');
}
?>

<form method="post" action="" class="form_sample">
<input type="radio" name="gender" value="male">男
<input type="radio" name="gender" value="female">女
<input type="submit" value="送信">
</form>

以下は、ラジオボタンを PHP で生成(label 要素も追加)し、value 属性は数値を指定する例です。

<?php
echo "選択された値 :";
if(isset($_POST['gender'])) {
  //$_POST['drinks']がすでに定義されている(値が送信されている)場合
  $gender = $_POST['gender'];
  if(ctype_digit($gender)){  //入力値は数字のみかを検証
    if($gender >= 1 && $gender <= 2){  //値の妥当性の検証
      switch($gender){
        case 1:
          echo '男<br>';
          break;
        case 2:
          echo '女<br>';
          break;
      }
    }
  }
}
?>

<form method="post" action="" class="form_sample">
<?php
$genders = array(1 => '男', 2 => '女');
foreach($genders as $key => $value) {
  echo '<label><input type="radio" name="gender" value="'.
  $key . '" />'  . $value . '</label>' . "\n";
}
?>
<br><input type="submit" value="送信">
</form>
テキストエリア

複数行のテキスト入力欄を作成する場合は、インライン要素の textarea 要素を利用できます。

1行テキスト入力フィールド(input 要素 type="text")との違いは、入力欄内で任意の改行ができることです。

但し、HTMLでは改行は反映されないので、HTML 上で改行をするには、nl2br() 関数を使って改行を br タグに変換する必要があります。

また、textarea 要素は input 要素とは異なり、開始タグと終了タグのペアで使います。タグの間に記述された内容が初期値として使われます。データの名前を指定する name 属性のほかに、入力エリアの行数、列数を示す rows 属性と cols 属性を指定します。

<textarea name="comment" rows="3" cols="30">この文字列が初期値として表示されます。</textarea>
  • name 属性:フォーム部品を識別するための名前を指定(連想配列のキーとなる)
  • rows 属性:入力欄の高さを行数で指定します。入力内容がこの高さを超えた場合は、入力欄にスクロールバーが表示されます。
  • cols 属性:入力欄の横幅を文字数で指定します。

rows 属性、cols 属性を指定した場合でも、ブラウザにより見た目の高さは異なります。

以下はテキストエリアのサンプルです。

<?php
//$_POST['comment']がすでに定義されている(値が送信されている)場合
if(isset($_POST['comment'])) {
  //エスケープ処理
  $textarea = htmlspecialchars($_POST['comment'], ENT_QUOTES, 'UTF-8');
  //改行タグの挿入
  $textarea_nl2br = nl2br($textarea);
}else{  //$_POST['comment']が未定義の場合
  $textarea = "この文字列が初期値として表示されます。";
  $textarea_nl2br = "";
}
?>

<form method="post" action="">
<p> テキストエリア(rows = 3, cols = 50 を指定)</p>
<textarea name="comment" rows="3" cols="50"><?= $textarea ?></textarea>
<input type="submit" value="送信">
</form>
<?php echo '<p>送信された値:<br>'. $textarea_nl2br . '</p>'; ?>

form の action 属性が「""」なので、自分自身(現在のファイル)に送信します。つまり、フォームも PHP の処理も同じファイルに記述されています。

form の method 属性が「post」なので送信されたデータは、$_POST という連想配列(スーパーグローバル変数)に格納されます。

テキストボックスの name 属性が「comment」なのでスーパーグローバル変数のキーに「comment」を指定すると($_POST['comment'] )入力された値を取得することができます。

テキストエリアに受け取った値を変数「$textarea」に代入し、echo の短縮(省略)形で設定しています。これにより、再度入力しなおす場合などに、入力した値を残すことができます。

  • 3行目:isset() を使って $_POST['comment'] がすでに定義されているかを調べます。
  • 5行目:すでに定義されている場合は、送信された値を htmlspecialchars() でエスケープ処理して変数「$textarea」に代入しています。
  • 7行目:nl2br() 関数を使って、入力された文字列に含まれる改行文字の前に <br /> を挿入しています。
  • 16行目:入力された文字列をエスケープ処理した値を echo の短縮(省略)形で設定しています。。
  • 19行目:echo で改行文字を挿入した値を出力しています。
セレクトメニュー

セレクトメニューは複数の項目から任意の項目を選択するときに使用します。

セレクトメニューを作成するには、インライン要素の select 要素と option 要素を利用します。

メニューの選択肢は option 要素を使って記述します。選択された option 要素の内容が、select 要素の name 属性で指定した名前のデータとして送信されます。

select 要素の属性

  • name 属性:フォーム部品を識別するための名前を指定します。この属性の値は、option 要素の値とセットで送信されます。
  • size 属性:メニューの表示行数を指定します。この属性の値に 1 を指定するとプルダウン形式のメニューになり、2 以上を指定するとリスト形式のメニューになります。
<select name="places1" size="1">
  <option value="manhattan">Manhattan</option>
  <option value="brooklyn">Brooklyn</option>
  <option value="queens">Queens</option>
  <option value="bronx">Bronx</option>
  <option value="staten">Staten Island</option>
</select>
  • multiple 属性:この属性を指定すると、メニューの中から複数の項目を選択できるようになります。(Shift または Ctrl キーを押しながらクリックすると、複数を選択することができます。)
<select name="places2" size="5" multiple>
  <option value="manhattan">Manhattan</option>
  <option value="brooklyn">Brooklyn</option>
  <option value="queens">Queens</option>
  <option value="bronx">Bronx</option>
  <option value="staten">Staten Island</option>
</select>

option 要素

select 要素の子要素として、各項目を option 要素で定義します。以下のような属性を指定できます。

  • value 属性:その項目が選択されたときにサーバーに送信される値を指定します。この値が指定されていない場合は、option 要素の内容(option 要素内のテキスト)が送信されます。
  • selected 属性:その項目を初期状態で選択されている状態にします。select 要素に multiple 属性が指定されている場合は、複数の選択肢に selected 属性を指定することができます。
  • disabled 属性:この属性が指定された部品は、選択することができなくなります。
<select name="places3" size="1">
  <option value="manhattan">Manhattan</option>
  <option value="brooklyn" selected>Brooklyn</option>
  <option value="queens">Queens</option>
  <option value="bronx" disabled>Bronx</option>
  <option value="staten">Staten Island</option>
</select>

以下は、セレクトメニューから送られたデータを受け取る例です。(multiple 属性の指定なし)

<?php
echo "選択された値 :";
if(isset($_POST['places'])) {
  //$_POST['places']がすでに定義されている(値が送信されている)場合
  echo htmlspecialchars($_POST['places'], ENT_QUOTES, 'UTF-8');
}
?>

<form method="post" action="" class="form_sample">
<select name="places" size="1">
  <option value="manhattan">Manhattan</option>
  <option value="brooklyn">Brooklyn</option>
  <option value="queens">Queens</option>
  <option value="bronx">Bronx</option>
  <option value="staten">Staten Island</option>
</select>
<input type="submit" value="送信">
</form>

以下は、select 要素に multiple 属性が設定されているセレクトメニューから送られたデータを受け取る例です。

multiple 属性が設定されている場合、複数の値を選択可能なため、select 要素の name 属性の値に [] を付けて、配列としてデータを送信します。

<?php
echo "選択された値 :";
if(isset($_POST['places'])) {
  //$_POST['places']がすでに定義されている(値が送信されている)場合
  foreach($_POST['places'] as $key => $value) {
    echo $key . ": " .htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . " ";
  }
}
?>

<form method="post" action="" class="form_sample">
<select name="places[]" size="5" multiple>
  <option value="manhattan">Manhattan</option>
  <option value="brooklyn">Brooklyn</option>
  <option value="queens">Queens</option>
  <option value="bronx">Bronx</option>
  <option value="staten">Staten Island</option>
</select>
<input type="submit" value="送信">
</form>

セレクトメニューを PHP で生成

以下は、PHP でセレクトメニューを生成する例です。

<form method="post" action="">
<select name="places[]" size="5" multiple>
<?php
$places = array(1 => 'Manhattan', 2 => 'Brooklyn', 3 => 'Queens', 4 => 'Bronx', 5 => 'Staten Island');
foreach($places as $key => $value) {
  echo '<option value="'. $key . '">'. $value . '</option>' . "\n";
}
?>
</select>
<input type="submit" value="送信">
</form>

以下のようなフォームが出力されます。

<form method="post" action="" class="form_sample">
<select name="places[]" size="5" multiple>
  <option value="1">Manhattan</option>
  <option value="2">Brooklyn</option>
  <option value="3">Queens</option>
  <option value="4">Bronx</option>
  <option value="5">Staten Island</option>
</select>
<input type="submit" value="送信">
</form>

以下は、PHP でセレクトメニューを生成して、セレクトメニューから送られたデータを受け取る例です。

<?php
echo "選択された値 :";
if(isset($_POST['places'])) {
  //$_POST['places']がすでに定義されている(値が送信されている)場合
  $places_selected = $_POST['places'];
  if(is_array($places_selected)){  //配列かどうかのチェック
    foreach($places_selected as  $value){
      if(ctype_digit($value)){  //入力値(配列の要素)は数字のみかを検証
        if($value >= 1 && $value <= 5){  //値の妥当性の検証(1以上3以下)
          switch($value){
            case 1:
              echo 'Manhattan ';
              break;
            case 2:
              echo 'Brooklyn ';
              break;
            case 3:
              echo 'Queens ';
              break;
            case 4:
              echo 'Bronx ';
              break;
            case 5:
              echo 'Staten Island ';
              break;
          }
        }
      }
    }
  }
}
?>

<form method="post" action="">
<select name="places[]" size="5" multiple>
<?php
$places = array(1 => 'Manhattan', 2 => 'Brooklyn', 3 => 'Queens', 4 => 'Bronx', 5 => 'Staten Island');
foreach($places as $key => $value) {
  echo '<option value="'. $key . '">'. $value . '</option>' . "\n";
}
?>
</select>
<input type="submit" value="送信">
</form>
日付入力フォーム

セレクトメニューを使って、日付入力のフォームを生成する方法です。

select 要素の option 要素を for 文を使って繰り返し処理することでより少ないコードで記述することができます。

「年」の生成には date() 関数で「Y」を指定して現在の「年(4桁)」を取得します。

30行目では、checkdate() 関数を使って日付の妥当性を検証しています。この関数は引数に数値を取るのである意味値の検証にもなります。但し、出力する際は独自に定義した h() 関数でエスケープ処理しています。

<form method="post" action="">
<?php
echo '<select name="year">'. "\n";
$start = date('Y') + 30;  //30年後の年
$end = date('Y') - 30;  //30年前の年
for($i = $start; $i >= $end; $i--) {
  echo '<option value="' .$i . '">' . $i .'</option>'. "\n";
}
echo '</select>年' . "\n";
echo '<select name="month">' . "\n";
for ($i = 1; $i <= 12; $i++) {
  echo '<option value="' .$i . '">' . $i .'</option>'. "\n";
}
echo '</select>月' . "\n";
echo '<select name="day">' . "\n";
for ($i = 1; $i <= 31; $i++) {
  echo '<option value="' .$i . '">' . $i .'</option>'. "\n";
}
echo '</select>' . "\n";
?>
<input type="submit" value="送信">
</form>

<?php
if(isset($_POST['year']) && isset($_POST['month']) && isset($_POST['day'])) {
  $year = @$_POST['year'];
  $month = @$_POST['month'];
  $day = @$_POST['day'];

  if(checkdate($month, $day, $year)) {
    echo '<p>'. h($year). '年'. h($month). '月'. h($day). '日は有効な日付です。 </p>';
  }else{
    echo '<p>'. h($year). '年'. h($month). '月'. h($day). '日は無効な日付です。 </p>';
  }
}

function h($str) {
  if($str === null) return '';
  return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?> 

checkdate() 関数

checkdate() 関数は、グレゴリオ暦の日付/時刻の妥当性をチェックし、指定した日付が有効な場合に TRUE、そうでない場合に FALSE を返します。以下が構文です。

bool checkdate( $month , $day , $year )

以下がパラメータです。

  • $month(int):月は 1 から 12 の間となります。
  • $day(int):日は、指定された month の日数の範囲内になります。year がうるう年の場合は、それも考慮されます。
  • $year(int):年は 1 から 32767 の間となります。

以下は、日付入力フォームを使って年齢を算出する例です。

<form method="post" action="">
<p>生年月日を入力して送信ボタンをクリックすると、年齢を表示します。</p>
<?php
echo '<select name="year">'. "\n";
$start = date('Y');  //今年
$end = date('Y') - 150;  //150年前
for($i = $start; $i >= $end; $i--) {
  echo '<option value="' .$i . '">' . $i .'</option>'. "\n";
}
echo '</select>年' . "\n";
echo '<select name="month">' . "\n";
for ($i = 1; $i <= 12; $i++) {
  echo '<option value="' .$i . '">' . $i .'</option>'. "\n";
}
echo '</select>月' . "\n";
echo '<select name="day">' . "\n";
for ($i = 1; $i <= 31; $i++) {
  echo '<option value="' .$i . '">' . $i .'</option>'. "\n";
}
echo '</select>' . "\n";
?>

<input type="submit" value="送信">
</form>

<?php
if(isset($_POST['year']) && isset($_POST['month']) && isset($_POST['day'])) {
  $year = $_POST['year'];
  $month = $_POST['month'];
  $day = $_POST['day'];

  if(checkdate($month, $day, $year)) {
    $now = date("Ymd");
    if($month < 10) {  //YYYYMMDD の形式にするために調整
      $month = '0' . h($month);
    }
    if($day < 10) {  //YYYYMMDD の形式にするために調整
      $day = '0'. h($day);
    }
    $date = h($year). $month. $day;
    $age = floor(($now-$date)/10000);
    echo '<p>'. h($year). '年'. h($month). '月'. h($day). '日生まれの年齢は' . $age . '才です</p>';
  }else{
    echo '<p>'. h($year). '年'. h($month). '月'. h($day). '日は無効な日付です。 </p>';
  }
}

function h($str) {
  if($str === null) return '';
  return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?>

以下は、生年月日から年齢を算出する簡単な計算式です。(厳密に言いと1日ずれる場合があるようです)

(今日の日付-誕生日の日付)/10000  //小数点以下切捨て
//誕生日が1977年3月17日の場合
((date('Ymd')-19770317)/10000);

jQuery を使って「日」の出力を調整

前述のサンプルでは「日」は、1~31 を常に表示するようにしていましたが、「年」または「月」のセレクトメニューの選択を変更すると、その「年」と「月」の選択状況に応じて「日」を表示するようにします。

そのためには、「年」または「月」のセレクトメニューの選択が変更されたイベントを検知して、「日」の出力を変更する必要があります。

「日」の出力は、以下のような PHP ファイル(setDays.php)を用意します。

変数「$last_day」には、選択された「年」と「月」から date() 関数mktime() 関数を使ってその月の最後の日付を取得して格納しています。

mktime() 関数の「日」を「0」に指定することで、前の月の最後の日を取得することができます。

そして変数「$last_day」を使って、for 文で option 要素を出力します。

setDays.php

<?php
$year = htmlspecialchars($_GET['year'], ENT_QUOTES, 'UTF-8');
$month = htmlspecialchars($_GET['month'], ENT_QUOTES, 'UTF-8');
$day = htmlspecialchars($_GET['day'], ENT_QUOTES, 'UTF-8');
$last_day = date('j', mktime(0, 0, 0, $month + 1, 0, $year)) ;

for ($i = 1; $i <= $last_day; $i++) {
  if($i == $day) {
    echo '<option value="' .$i . '" selected>' . $i .'</option>'. "\n";
  }else{
    echo '<option value="' .$i . '">' . $i .'</option>'. "\n";
  }
}
?>  

jQuery の change() イベントを使って、セレクトメニューが変更されたら「日」のセレクトメニューに前述の setDays.php を load() を使って取り込みます。

また、前述のサンプルでは POST を使用しましたが、この例では GET を使用します。そのため、load() の第2パラメータには、フォーム(id 属性が getBirthday)の値(value)をシリアライズ(serialize())した文字列を渡します。

6行目は、シリアライズされた値がどのようなものかを表示するものです。

<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<script type="text/javascript">
jQuery(function($){
  $('select').change(function(event) {
    $('#day').load('post_16_2.php', $('#getBirthday').serialize());
    $('#sdata').text($('#getBirthday').serialize());
  });
});
</script>   

以下がサンプルになります。このサンプルでは、セレクトメニューで選択された値を使って option 要素に selected 属性を追加して選択状態にさせています。また、それぞれのフォーム部品には jQuery で操作しやすいように id 属性を付与しています。

<body>
<p>生年月日を入力して送信ボタンをクリックすると、年齢を表示します。</p>
<form method="get" action="" id="getBirthday">
<?php
$year = @$_GET['year'];
$month = @$_GET['month'];
$day = @$_GET['day'];
echo '<select id="year" name="year">'. "\n";
$start = date('Y');
$end = date('Y') - 150;
for($i = $start; $i >= $end; $i--) {
  if($i == $year) {
    echo '<option value="' .$i . '" selected>' . $i .'</option>'. "\n";
  }else{
    echo '<option value="' .$i . '">' . $i .'</option>'. "\n";
  }
}
echo '</select>年' . "\n";
echo '<select id="month" name="month">' . "\n";
for ($i = 1; $i <= 12; $i++) {
  if($i == $month) {
    echo '<option value="' .$i . '" selected>' . $i .'</option>'. "\n";
  }else{
    echo '<option value="' .$i . '">' . $i .'</option>'. "\n";
  }
}
echo '</select>月' . "\n";
echo '<select id="day" name="day">' . "\n";

if(isset($_GET['year']) && isset($_GET['month']) && isset($_GET['day'])) {
  $last_day = date('j', mktime(0, 0, 0, $month + 1, 0, $year)) ;
}else{
  $last_day = 31;
}
for ($i = 1; $i <= $last_day; $i++) {
  if($i == $day) {
    echo '<option value="' .$i . '" selected>' . $i .'</option>'. "\n";
  }else{
    echo '<option value="' .$i . '">' . $i .'</option>'. "\n";
  }
}
echo '</select>' . "\n";
echo '<input type="submit" value="送信">';

?>
</form>

//以下のPHPは前述のサンプルと同じです。
<?php
if(isset($_GET['year']) && isset($_GET['month']) && isset($_GET['day'])) {
  $year = $_GET['year'];
  $month = $_GET['month'];
  $day = $_GET['day'];

  if(checkdate($month, $day, $year)) {
    $now = date("Ymd");
    if($month < 10) {
      $month = '0' . h($month);
    }
    if($day < 10) {
      $day = '0'. h($day);
    }
    $date = h($year). $month. $day;
    $age = floor(($now-$date)/10000);
    echo '<p>'. h($year). '年'. h($month). '月'. h($day). '日生まれの年齢は' . $age . '才です</p>';
  }else{
    echo '<p>'. h($year). '年'. h($month). '月'. h($day). '日は無効な日付です。 </p>';
  }
}

function h($str) {
  if($str === null) return '';
  return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?>

<p>serialize の値:<span id="sdata"></span></p>

<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<script type="text/javascript">
jQuery(function($){
  $('select').change(function(event) {
    $('#day').load('setDays.php', $('#getBirthday').serialize());
    $('#sdata').text($('#getBirthday').serialize());
  });
});
</script>
</body>

※ mktime() 関数の「年(year)」 として有効な範囲は 1901 から 2038 の間なのでそれ以前の場合は、「日」がうまく調整できません。

フォームの検証(バリデーション)

セキュリティ対策として、以下の3つを正しく行うことで多くの脆弱性を減らすことができます。

  • 入力値の厳格な検証
  • 出力時の適切なエスケープ処理
  • 多重防御

入力値の厳格な検証

外部からの入力データは、入力値が想定される範囲の値であることを厳格に検証します。

例としては、ユーザーが年齢を入力する場合、想定される入力値は正の整数で、かつ最大値は130ぐらいです。この場合、0 以上 130 以下の整数以外はエラーとして処理を継続しないようにします。このように入力値を検証しておけば、攻撃文字列を入力されることを防ぐことができます。

また、検証でエラーとなる場合、システム側で入力値を修正して処理を継続しないことが重要です。

エラーとなった場合は、エラー処理としてユーザーにエラーを表示したり、明らかに攻撃だとわかればエラーを記録して即座にプログラムを終了するようにします。

そしてどのデータが外部から来たものかを常に意識する必要があります。

ユーザーからの入力である $_GET, $_POST, $_COOKIE は全て外部からの入力データになります。またデータベースからのデータでもユーザーからの入力値が含まれる場合は、外部からの入力データになります。

出力時の適切なエスケープ処理

入力されたデータをブラウザやデータベース等に出力する場合、必ずエスケープ処理してから出力するようにします。

この処理を行わないと、XSS(クロスサイトスクリプティング)の脆弱性が発生するので、必ず行うようにします。

多重防御

1つの脆弱性対策だけではなく、複数の対策を行うことで、1つの対策が失敗しても別の対策により防御できる可能性が高くなります。

入力値の検証の必要性

攻撃者はブラウザを使わずにサーバーにどんな値でも送信することが可能です。

例えば、ラジオボタンのようなブラウザで選択できる値が固定されているものでも、攻撃者はそれ以外の値をサーバーに送信することができます。

input タグに maxlength 属性を指定していても、攻撃者は制限を越えた文字数のデータをサーバーに送信できてしまいます。

このため全ての入力値が、想定される範囲の値であるかどうかを検証することが Web アプリケーションの基本になります。

また、JavaScript によりクライアント側(ブラウザ)で検証していても、必ずサーバー側で検証する必要があります。攻撃者はどのようなリクエストでもサーバーに送信することができるため、JavaScript での検証を簡単にすり抜けることが可能だからです。

パラメータ改竄(かいざん)

パラメータ改竄とは、ブラウザからのリクエストの一部を改竄することです。

URL に含まれるクエリ文字列、フォームの隠しフィールドの値、ラジオボタンなどの選択肢の値、Cookie に含まれるデータ、ブラウザ情報、リファラー(参照元)情報などは、悪意のある攻撃者であれば変更することができます。

以下は全て誤解です。

  • フォームの隠しフィールドの値は変更されることはない。(
  • ラジオボタンやチェックボックスの選択肢の値は変更されることはない。(
  • セレクトメニューで指定した値は変更されることはない。(
  • maxlength 属性で最大文字数を指定すれば、それ以上の文字列は送信されない。(
  • Cookie に含まれるデータは変更されることはない。(
  • $_SERVER['HTTP_USER_AGENT'] の値は信頼できる。(
  • $_SERVER['HTTP_REFFERER'] の値は信頼できる。(
  • $_SERVER['PHP_SELF'] の値は信頼できる。(

これらの値を使用する場合は、値を検証し、出力の際は適切にエスケープ処理する必要があります。

ファイル名の操作での注意

ファイル名の指定で「../」と入力すると、1つ上の階層のディレクトリを意味します。外部からファイル名を指定する場合、このような文字列をチェックしないと、想定外の場所にあるファイルにアクセスされる危険性があります。

ディレクトリをさかのぼって想定外のファイルを読み書きすることを「ディレクトリトラバーサル」と言います。

外部からファイル名を指定する(外部からの入力値をファイル名の生成に使用する)ような設計をしないことが一番の対策です。

また、include 文や require 文を使って他のファイルを読み込むことがありますが、これらの引数にユーザーからの入力値が含まれる場合もセキュリティ上の問題が生じることがあります。ユーザーからの入力値をそのまま include 文の引数にしてしまうと攻撃者は自由にコードをサーバー上で実行できてしまうためとても危険です。

対策としては、外部からの値を include 文の引数に指定するようなコードを書かないことです。

入力値の検証

文字エンコードのチェック

ブラウザなど外部から入力されたデータは、文字エンコードが正しいかをチェックする必要があります。文字エンコードが違っていたり壊れていると、想定外の動作になる場合があり脆弱性になる可能性があります。

対策としては入力時のバリデーション処理で,すべての文字列に対してエンコーディングが正しいかチェックします。

PHP 5.2.1 以降では mb_check_encoding() 関数で簡単に文字エンコードをチェックすることができます。

以下が mb_check_encoding() 関数の構文です。

bool mb_check_encoding($var,  $encoding)

この関数は文字列が、指定したエンコーディングで有効なものかどうかを調べます。

パラメータ

  • $var(string):調べる文字列(バイトストリーム)
  • $encoding(string):期待するエンコーディング。デフォルトは mb_internal_encoding()

戻り値

成功した場合に TRUE を、失敗した場合に FALSE を返します。

以下はエンコーディングをチェックする関数の例です。

mb_check_encoding() 関数でエンコーディングをチェックして、エンコーディングが正しくない場合は、die() を使ってメッセージを表示して現在のプログラムを終了します。

function checkEncoding($val) {
  if (! mb_check_encoding($val, 'UTF-8')) {
    die('文字エンコーディングの不正');
  }
  return $val;
}

die()exit() と同じです。

以下は、更に mb_ereg() 関数を使って文字種と文字数もチェックする例です。第2パラメータには、文字種と文字数をチェックする正規表現パターンを渡します。

// 文字エンコーディング、文字種、文字数のチェック
function checkValue($val, $pattern) {
  if (! mb_check_encoding($val, 'UTF-8')) {
    die('文字エンコーディングの不正');
  }
  if (! mb_ereg($pattern, $val)) {
    die('文字種または文字数が範囲外です');
  }
  return $val;
}

//呼び出し例(制御文字以外で20文字以下)
$checked_val = checkValue(@$_GET['text_input'], "\\A[[:^cntrl:]]{0,20}\\z");

参考:
PHP以外では既にあたり前になりつつある文字エンコーディングバリデーション
文字エンコーディングとセキュリティ(2)

不正な値のチェック

入力されたデータ(値)に不正な値が含まれていないかをチェックする必要があります。

magic_quotes_gpc

magic_quotes_gpc の設定が ON の場合、GET, POST, COOKIE データに含まれるシングルクォート、ダブルクォート、バックスラッシュ及び NULL 文字の直前に、「\」(バックスラッシュ)が追加されます。

これは初心者が必要なエスケープ処理を忘れ、危険なコードが実行されないように意図されたものですが、機能が不完全で副作用も多いため、PHP 5.3.0 で非推奨になりました。

get_magic_quotes_gpc() 関数を使って、現在の magic_quotes_gpc の設定を取得し、ON の場合のみ、stripslashes() 関数で「\」(バックスラッシュ)を削除することで、GET, POST, COOKIE に追加された「\」を安全に取り除くことができます。

※ PHP 5.4.0 でマジッククォート機能は削除され、get_magic_quotes_gpc() 関数は常に FALSE 返すようになりました。

※ PHP 7.4.x で get_magic_quotes_gpc() は非推奨になりました。

NULLバイト攻撃対策

NULL バイト攻撃とは NULL バイト(文字コードの値が0の文字)を使いプログラムを誤作動させる攻撃です。

対策は、入力された全ての文字列データに対して、NULL バイトが含まれていないかを、バイナリーセーフな関数(preg_match() )でチェックします。

通常使われる日本語の文字データには、NULL バイトは含まれていないので、NULL バイトが含まれている入力は不正な入力になるので、検知したら処理を直ちに中止するようにします。

以下は、preg_match() を使っての NULL バイト攻撃対策の関数の例です。

function checkNull($val) {
  if(preg_match('/\0/', $var)){  //NULLバイト攻撃対策
    die('不正な入力です。');
  }
}

制御文字

制御文字(control character)とは、文字コードの規格で定義される文字のうちディスプレイ、プリンター、通信装置などに対して、特別な動作(制御)をさせるために使う文字です。

通常の入力では、改行やタブ以外の制御文字は許可しないように検証します。

制御文字は正規表現では、[[:cntrl:]] で表されます。

[\r\n\t[:^cntrl:]] とすると、改行(\r\n)とタブ(\t)を含む制御文字以外の文字となります。

※ 但し、バイナリーデータには制御文字が含まれる可能性があるので、その場合はチェックの対象から外します。

以下は、magic_quotes_gpcが「on」の場合の対策、NULLバイト攻撃対策、改行やタブ以外の制御文字、文字エンコードのチェックをまとめて行う関数の例です。

function checkInput($var){
  if(is_array($var)){
    return array_map('checkInput', $var);
  }else{
    //PHP 7.4.x で get_magic_quotes_gpc() は非推奨になりました
    //php.iniでmagic_quotes_gpcが「on」の場合の対策
    /*if(get_magic_quotes_gpc()){
      $var = stripslashes($var);
    }*/
    if(preg_match('/\0/', $var)){  //NULLバイト攻撃対策
      die('不正な入力です。');
    }
    if(!mb_check_encoding($var, 'UTF-8')){ //文字エンコードのチェック
      die('不正な入力です。');
    }
    //改行、タブ以外の制御文字のチェック
    if(preg_match('/\A[\r\n\t[:^cntrl:]]*\z/u', $var) == 0){
      die('不正な入力です。制御文字は使用できません。');
    }
    return $var;
  }
}

$_GET などの値は、配列になっている可能性があるので、is_array() で配列かどうかを調べ、配列の場合は array_map() 関数で checkInput() を配列に適用しています。

array_map() 関数

以下が構文です。

array array_map ( $callback , $array )

array_map() は、第2パラメータの配列($array)の各要素に第1パラメータの関数($callback)を実行した後、 その全ての要素を含む配列を返します。


callback 関数が受け付けるパラメータの数は、 array_map() に渡される配列の数に一致している必要があります。

パラメータ

  • $callback(callable):配列の各要素に適用するコールバック関数の名前(関数名)
  • $array(array):コールバック関数を適用する配列

戻り値

array の各要素に callback 関数を適用した後、 その全ての要素を含む配列を返します。

以下は、配列が渡された場合でも、それらの値に対してエスケープ処理を行う関数の例です。

function h($str){
  if(is_array($str)){
    //$strが配列の場合、h()関数をそれぞれの要素について呼び出す
    return array_map('h', $str);
  }else{
    if($str === null) return '';
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
  }
} 

以下は前述の関数を使用したサンプルです。

<?php
function checkInput($var){
  if(is_array($var)){
    return array_map('checkInput', $var);
  }else{
    //PHP 7.4.x で get_magic_quotes_gpc() は非推奨になりました
    //php.iniでmagic_quotes_gpcが「on」の場合の対策
    /*if(get_magic_quotes_gpc()){
      $var = stripslashes($var);
    }*/
    //NULLバイト攻撃対策
    if(preg_match('/\0/', $var)){
      die('不正な入力です。');
    }
    //文字エンコードのチェック
    if(!mb_check_encoding($var, 'UTF-8')){
      die('不正な入力です。');
    }
    //文字数(0-100)と改行以外の制御文字のチェック
    if(preg_match('/\A[\r\n[:^cntrl:]]{0,100}\z/u', $var) === 0){
      die('文字数は100文字以下でお願いします。制御文字は使用できません。');
    }
    return $var;
  }
}

function h($str){
  if(is_array($str)){
     //$strが配列の場合、h()関数をそれぞれの要素について呼び出す
     return array_map('h', $str);
  }else{
    if($str === null) return '';
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
  }
}

if(isset($_GET['text_input'])) {
  $text_input = h(checkInput(@$_GET['text_input']));
  $nl2br = nl2br($text_input);
}else{
  $text_input = "";
}
?>

<form action="" method="get">
<textarea name="text_input" rows="3" cols="30"><?= $text_input ?></textarea>
 <input type="submit" ><input type="reset" >
</form>
<p>入力テキスト:<br><?php echo @$nl2br; ?></p>

複数のファイルで検証やエスケープ処理の関数を使用する場合は、それらの関数を1つのファイルにまとめておき、それぞれのファイルで require 文を使って読み込むようにします。以下がサンプルです。

//validation.php 検証やエスケープ処理の関数をまとめたファイル
<?php
function checkInput($var){
  if(is_array($var)){
    return array_map('checkInput', $var);
  }else{
  //PHP 7.4.x で get_magic_quotes_gpc() は非推奨になりました
    //php.iniでmagic_quotes_gpcが「on」の場合の対策
    /*if(get_magic_quotes_gpc()){
      $var = stripslashes($var);
    }*/
    //NULLバイト攻撃対策
    if(preg_match('/\0/', $var)){
      die('不正な入力です。');
    }
    //文字エンコードのチェック
    if(!mb_check_encoding($var, 'UTF-8')){
      die('不正な入力です。');
    }
    //文字数(0-100)と改行以外の制御文字のチェック
    if(preg_match('/\A[\r\n[:^cntrl:]]{0,100}\z/u', $var) === 0){
      die('文字数は100文字以下でお願いします。また制御文字は使用できません。');
    }
    return $var;
  }
}

function h($str){
  if(is_array($str)){
     //$varが配列の場合、h()関数をそれぞれの要素について呼び出す
     return array_map('h', $str);
  }else{
    if($str === null) return '';
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
  }
}   
//sample1.php
<body>
<?php
require 'validation.php';  //関数のファイルの読み込み

if(isset($_GET['text_input'])) {
  $text_input = checkInput($_GET['text_input']);
}else{
  $text_input = "";
}
?>
<form action="sample2.php" method="get">
<textarea name="text_input" rows="3" cols="30"><?= h($text_input) ?></textarea><br>
 <input type="submit" ><input type="reset" >
</form>
</body>
//sample2.php
<body>
<?php
require 'validation.php';  //関数のファイルの読み込み

if(isset($_GET['text_input'])) {
  $text_input = h(checkInput($_GET['text_input']));
  $nl2br = nl2br($text_input);
}else{
  die('不正なアクセスの可能性があります。');
}
?>
<p>入力テキスト:<br><?php echo $nl2br; ?></p>
</body>
数値かどうかのチェック

入力された内容が、数字のみの文字列かどうかをチェックするには、ctype_digit() 関数を使用します。

bool ctype_digit( $text )

ctype_digit() 関数は、与えられた文字列の全ての文字が数字であるかどうかを調べ、すべての文字が 10 進数字だった場合に TRUE、そうでない場合に FALSE を返します。

以下は、サンプルです。

<?php
//エラーがない場合は「0」を、ある場合は「1」を代入し結果を表示する際のフラグとする
$error = 0;
//$_POST['gender']が定義されているかどうか
if(isset($_POST['gender'])){
  //$genderの値が数字かどうか確認
  $gender = $_POST['gender'];
  if(ctype_digit($gender)){
    if($gender == 1) {
      $gendername = '男性';
    }elseif($gender == 2) {
      $gendername = '女性';
    }else{
      $error = 1;  //$genderに1、2以外の場合はエラー
    }
  }else{
    $error = 1;  //$genderが数字でない場合はエラー
  }
}else{
  $error = 1;  //$_POST['gender']が未定義の場合はエラー
}

if($error == 0) {
  echo $gendername;
}else{
  if(!isset($_POST['gender'])){
    echo "未定義です。";
  }else{
    echo "不正な値です。";
  }
}
?>

<form action="" method="post">
<label><input type="radio" name="gender" value="1">男性</label>
<label><input type="radio" name="gender" value="2">女性</label>
<input type="submit" >
</form>

value 属性を数値(整数)に限定」も参照ください。

また、is_numeric() 関数でも、数字または数値形式の文字列かどうかをチェックできますが、この関数は、16進数(0xACなど)や指数表記の数字(5e10など)も TRUE を返します。想定する値により使い分けるようにします。

範囲のチェック

数値の場合は、最大値、最小値が指定できる場合は、それらのチェックを行います。

前述の例では、想定される値(1または2)以外の場合はエラーにしています。

$gender = $_POST['gender'];
if(ctype_digit($gender)){
  if($gender == 1) {
    $gendername = '男性';
  }elseif($gender == 2) {
    $gendername = '女性';
  }else{
    $error = 1;  //$genderに1、2以外の場合はエラー
  }
  ......

文字列の場合も、文字数の最大値、最小値をチェックします。これらのチェックはできる限り厳格に行うようにします。

日本語の文字数(文字列の長さ)を調べるには、mb_strlen() 関数を使用します。

mb_strlen() 関数は、文字数を返します。

if(mb_strlen($var) > 50){
   die('文字数の制限(50文字)を越えています。');
}	

また、preg_match() を使って以下のように制御文字でないことと文字数のチェックを行う方法もよく使われます。

//制御文字でないことと文字数をチェック
if(preg_match('/\A[[:^cntrl:]]{1,30}\z/u', $name) == 0) {
  die('名前は30文字以内でお願いします。');
}

バリデーションサンプル

以下は、フォームの検証(バリデーション)のサンプルです。

2~26行目:入力値に不正なデータがないかなどをチェックする関数の定義です。詳細は「入力値の検証」を参照ください。

28行目:上記で定義した関数で、POST されたデータを全て検証します。

31~38行目:htmlspecialchars() を利用したエスケープ処理の関数の定義です。

43~46行目:isset() を使って、変数が定義されているかを調べ、定義されていればその値を変数に格納し、定義されていない場合は空文字列 '' を代入しています(以前は NULLで初期化していましたが、PHP8.1 から trim() に NULL を渡すとエラーになるので、空文字列に変更)。

初期化されていない変数を使用すると、 E_NOTICE レベルのエラーが発生します。フォーム内で value 属性に出力している変数は、ページを開いた時点ではデータがサーバーに送られていないので未定義になっていて、この初期化を行わないとエラーが表示されてしまいます。

49~53行目:ユーザーの入力値の前後のホワイトスペースを trim() 関数で取り除き、定義した h() 関数でエスケープ処理をしています。

55行目:エラーの文字列を格納するための変数を定義しています。エラーは複数発生する可能性があるので、配列にしています。

57~62行目:$_POST['name'](お名前) の値が空の場合は、「*お名前は必須です。」と言う文字列をエラー用の配列に追加します。また、preg_match() を使って、入力された文字列が制御文字以外の文字で1文字以上30文字以下かを検証しています。30文字以上の場合、または制御文字が含まれている場合は「*お名前は30文字以内でお願いします。」と言うエラーをエラー用の配列に追加します。

64~71行目:$_POST['email'](E-mail)の値を検証しています。空の場合は、「*E-mail アドレスは必須です。」と言う文字列をエラー用の配列に追加します。空でない場合は、正規表現を使ってメールアドレスの形式に合っているかを検証します。(詳細は「メールアドレスの検証」を参照ください)

73~95行目:$_POST['type'](セレクトメニューの選択された値)の検証です。値は数値なので ctype_digit() 関数で数値かどうかを検証して数値以外の場合はエラーにします。続いて値は 0~3 の範囲なので、それ以外の値であればエラーにします。また、値が「0」の場合は、選択肢を選んでいないのでエラーとしています。そして値により、それぞれに対応する文字列を変数「$type_value」に格納しています。

97~102行目:コメントの値を検証しています。「お名前」の検証とほぼ同じです。

104行目:isset() を使って、全ての POST された値が定義されているかを調べて、定義されていればそれ以降を実行します。

105行目:エラー用の配列 $error の要素の数を count() 関数を使って調べ、1以上の場合はエラーがあるので、エラーがあることを表示し、foreach() で配列の内容を出力します。エラーがなければ「送信しました。」と表示します。

<?php
//入力値に不正なデータがないかなどをチェックする関数
function checkInput($var){
  if(is_array($var)){
    //$var が配列の場合、checkInput()関数をそれぞれの要素について呼び出す
    return array_map('checkInput', $var);
  }else{
  //PHP 7.4.x で get_magic_quotes_gpc() は非推奨になりました
    //php.iniでmagic_quotes_gpcが「on」の場合の対策
    /*if(get_magic_quotes_gpc()){
      $var = stripslashes($var);
    }*/
    //NULLバイト攻撃対策
    if(preg_match('/\0/', $var)){
      die('不正な入力です。');
    }
    //文字エンコードのチェック
    if(!mb_check_encoding($var, 'UTF-8')){
      die('不正な入力です。');
    }
    //改行以外の制御文字及び最大文字数のチェック
    if(preg_match('/\A[\r\n[:^cntrl:]]{0,100}\z/u', $var) === 0){
      die('不正な入力です。最大文字数は100文字です。また、制御文字は使用できません。');
    }
    return $var;
  }
}
//POSTされたデータをチェック
$_POST = checkInput($_POST);

//エスケープ処理の関数
function h($str){
  if(is_array($str)){
     //$strが配列の場合、h()関数をそれぞれの要素について呼び出す
     return array_map('h', $str);
  }else{
    if($str === null) return '';
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
  }
}

//POSTされたデータを変数に格納(最初は入力データがないのでこの初期化をしないとエラーとなる)
$name = isset($_POST['name']) ? $_POST['name'] : '';
$email = isset($_POST['email']) ? $_POST['email'] : '';
$type = isset($_POST['type']) ? $_POST['type'] : '';
$comment = isset($_POST['comment']) ? $_POST['comment'] : '';

//POSTされたデータを整形(前後にあるホワイトスペースを削除)してエスケープ処理
$name = h(trim($name));
$email = h(trim($email));
$type = h(trim($type));
$type_value = "";  //セレクトメニューで選択された値の文字列を格納する変数
$comment = h(trim($comment));

$error = array();

if($name == '') {
  $error[] = '*お名前は必須です。';
  //制御文字でないことと文字数をチェック
}elseif(preg_match('/\A[[:^cntrl:]]{1,30}\z/u', $name) == 0) {
  $error[] = '*お名前は30文字以内でお願いします。';
}

if($email == ''){
  $error[] = '*E-mail アドレスは必須です。';
}else{   //メールアドレスを正規表現でチェック
  $pattern = '/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/uiD';
  if(!preg_match($pattern, $email)){
    $error[] = '*メールアドレスの形式が正しくありません。';
  }
}

if(ctype_digit($type)){  //入力値(配列の要素)は数字のみかを検証
  if($type >= 0 && $type <= 3){  //値の妥当性の検証(0以上3以下)
    switch($type){
      case 0:
        $type_value = '選択してください';
         $error[] = '*リストから種類を選択してください';
        break;
      case 1:
        $type_value = 'お問い合わせ';
        break;
      case 2:
        $type_value = '登録希望';
        break;
      case 3:
        $type_value = 'その他';
        break;
    }
  }else{
    $error[] = '*不正な入力です。(値が0~3ではない)';
  }
}else{
  $error[] = '*不正な入力です。(値が数値ではない)';
}

if($comment == '') {
  $error[] = '*コメントは必須です。';
  //制御文字(改行を除く)でないことと文字数をチェック
}elseif(preg_match('/\A[\r\n[:^cntrl:]]{1,100}\z/u', $comment) == 0) {
  $error[] = '*コメントは100文字以内でお願いします。';
}

if(isset($_POST['name']) && isset($_POST['email']) && isset($_POST['type']) && isset($_POST['comment'])) {
  if(count($error) > 0){
    echo '<p style="color:red;">以下のエラーがあります。</p><p style="color:red;">';
    foreach($error as $value) {
      echo $value . "<br>";
    }
    echo '</p>';
  }else{
    echo '<p style="color:green;">送信しました。</p>';
  }
}
?>

<form action="" method="post">
<div><span>お名前: </span>
  <input type="text" name="name" size="40" value="<?= $name ?>">
</div>
<div><span>E-mail: </span>
  <input type="text" name="email" size="40" value="<?= $email ?>">
</div>
<div>
<span>種類:</span>
<select name="type" size="1">
<?php
$types = array('選択してください', 'お問い合わせ', '登録希望',  'その他');
foreach($types as $key => $value) {
  if($type) {
    if($type == $key){
      echo '<option value="'. $key . '" selected>'. $value . '</option>' . "\n";
    }else{
      echo '<option value="'. $key . '">'. $value . '</option>' . "\n";
    }
  }else{
    echo '<option value="'. $key . '">'. $value . '</option>' . "\n";
  }
}
?>
</select>
</div>
<div><span>コメント:</span><br><textarea name="comment" rows="3" cols="30" ><?= $comment ?></textarea></div>
<div><input type="submit" ></div>
</form>
<div id="output">
<p><strong>入力値の確認</strong></p>
<p>お名前: <?= $name ?></p>
<p>E-mail:<?= $email ?></p>
<p>種類:<?= $type_value ?></p>
<p>コメント:<?= nl2br($comment) ?></p>
</div> 
jQuery (JavaScript) を使った検証を追加

前述のサンプルに jQuery (JavaScript) を使った検証を追加してみます。

PHP の検証部分は同じなので省略します。

各コントロール(input/textarea 要素)にはバリデーションの条件を示す「validate」「required」「mail」などの class 属性を付与します。

  • validate:検証対象の要素に付与するクラス
  • required:必須項目に付与するクラス
  • mail:メールアドレスを検証する項目に付与するクラス
  • max30, max100:文字の最大数を検証する項目に付与するクラス
  • not0:選択された項目の値が 0 かどうかを検証する項目に付与するクラス

CSS ではコントロールなどの基本スタイルを定義するとともに、以下のようなエラー(入力漏れやメールアドレスの入力ミスなど)用のスタイルを記述しておきます。

<style>
.error input ,
.error textarea {
    background-color: #F8DFDF;
}
p.error{
    margin:0;
    color:red;
    font-weight:bold;
    margin-bottom:1em;
}
</style>

この jQuery のスクリプトは、すべて submit イベント内に記述しています。submit イベントは送信ボタン(type 属性が submit または image の input 要素)がクリックされたときに発生するイベントです。

個々の検証については「jQuery フォームのバリデーション(検証)」を参照ください。

<form action="" method="post">
<div><span> お名前: </span>
  <input type="text" name="name" size="40" class="validate required max30" value="<?= $name ?>">
</div>
<div><span> E-mail: </span>
  <input type="text" name="email" size="40" class="validate required mail" value="<?= $email ?>">
</div>
<div>
<span>種類:</span>
<select name="type" size="1" class="validate not0">
<?php
$types = ['選択してください', 'お問い合わせ', '登録希望',  'その他'];
//PHP5.4 未満では、$types = array('選択してください', 'お問い合わせ', '登録希望',  'その他');
foreach($types as $key => $value) {
  if($type) {
    if($type == $key){
      echo '<option value="'. $key . '" selected>'. $value . '</option>' . "\n";
    }else{
      echo '<option value="'. $key . '">'. $value . '</option>' . "\n";
    }
  }else{
    echo '<option value="'. $key . '">'. $value . '</option>' . "\n";
  }
}
?>
</select>
</div>
<div><span>コメント:</span><br><textarea name="comment" rows="3" cols="30" class="validate required max100"><?= $comment ?></textarea></div>
<div><p><span id="count"> </span>/100</p></div>
<div><input type="submit" ></div>
</form>
<div id="output">
<p><strong>入力値の確認</strong></p>
<p>お名前: <?= $name ?></p>
<p>E-mail:<?= $email ?></p>
<p>種類:<?= $type_value ?></p>
<p>コメント:<?= nl2br($comment) ?></p>
</div>

<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<script type="text/javascript">
jQuery(function($){
  //エラーを表示する関数の定義
  function show_error(message, this$) {
    text = this$.parent().find('span').text() + message;
    this$.parent().append("<p class='error'>" + text + "</p>")
  }

  $("form").submit(function(){
    //エラー表示の初期化
    $("p.error").remove();
    $("div").removeClass("error");
    var text = "";

    //1行テキスト入力フォームとテキストエリアの検証
    $(":text,textarea").filter(".validate").each(function(){

      //必須項目の検証
      $(this).filter(".required").each(function(){
        if($(this).val()==""){
          show_error("は必須項目です。", $(this));
        }
      })

      $(this).filter(".max30").each(function(){
        if($(this).val().length > 30){
          show_error("は30文字以内です。", $(this));
        }
      })

      $(this).filter(".max100").each(function(){
        if($(this).val().length > 100){
          show_error("は100文字以内です。", $(this));
        }
      })

      //メールアドレスの検証
      $(this).filter(".mail").each(function(){
        if($(this).val() && !(/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/g).test($(this).val())){
          $(this).parent().prepend("<p class='error'>メールアドレスの形式が異なります</p>");
        }
      })
    })

    //セレクトメニューの検証
    $("select").filter(".validate").each(function(){
       $(this).filter(".not0").each(function(){
         if($(this).val() == 0 ) {
           show_error("を選択してください。", $(this));
         }
       });
    });

    //error クラスの追加の処理
    if($("p.error").size() > 0){
      $("p.error").parent().addClass("error");
      return false;
    }
  })

  //テキストエリアに入力された文字数を表示
  $("textarea").on('keydown keyup change', function() {
    var count = $(this).val().length;
    $("#count").text(count);
    if(count > 100) {
      $("#count").css({color: 'red', fontWeight: 'bold'});
    }else{
      $("#count").css({color: '#333', fontWeight: 'normal'});
    }
  });

});
</script>
</body>
</html>

アンケートフォーム

簡単なアンケートフォームのサンプルです。以下のようなページを作成します。

入力ページ(questionnaire1.php)
ユーザーがアンケートの項目を選択して送信するためのページ
完了ページ(questionnaire2.php)
アンケートの回答をテキストファイルに保存し、回答に不備があればエラーメッセージを表示するページ
集計ページ(questionnaire3.php)
アンケートの集計結果を表示するページ
入力ページ

以下の項目についてアンケートします。

  • 性別(ラジオボタン)
  • 年齢(セレクトメニュー)
  • 趣味(チェックボックス)

form 要素の action 属性には完了ページ(questionnaire2.php)を指定します。また、それぞれのフォーム部品の value 属性には数値を指定します。

「性別」はラジオボタンで作成します。

「年齢」はセレクトメニューで作成します。

最初と最後の option 要素は HTML で記述し、その他は for 文を使って出力します。最初の option 要素「選択してください。」の value 属性の値は「0」として、検証の際に利用します。

for 文の変数 $num をうまく利用して、年齢の表示と value 属性に使用することで簡潔に記述することができます。

「趣味」はチェックボックスで作成します。

配列「$hobby」はチェックボックス(input 要素)の value 属性($hobby の $key)と表示する値($hobby の $value)に利用します。

配列「$ids」は label 要素の for 属性とチェックボックス(input 要素)の ide 属性に使用します。(

以下が入力ページ(questionnaire1.php)のコードです。

questionnaire1.php

<h1>アンケート入力ページ</h1>

<form action="questionnaire2.php" method="post">
<div>
<p>性別</p>
  <input type="radio" name="gender" id="male" value="1">
    <label for="male"> 男性 </label>
  <input type="radio" name="gender" id="female"  value="2">
    <label for="female"> 女性 </label>
</div>
<div>
<label for="age"> 年齢 </label>
<select name="age" id="age">
<option value="0" selected>選択してください。</option>
<?php
for($num = 1; $num <= 7; $num++) {
  echo '<option value="' . $num . '">' . $num . '0代</option>' . "\n";
}
?>
<option value="8">80代以上</option>
</select>
</div>
<div>
<p>趣味</p>
<?php
$hobby = array(0 => "音楽",
               1 => "スポーツ",
               2 => "車",
               3 => "アート",
               4 => "旅行",
               5 => "カメラ",
               6 => "読書",
               7 => "その他");
$ids = array('music', 'sport', 'car', 'art', 'travel', 'camera', 'book', 'other');
foreach($hobby as $key => $value) {
  echo '<label for="' . $ids[$key] .'"><input type="checkbox" name="hobby[]" value="'
  .$key . '" id="' . $ids[$key] . '">' . $value . '</label>' . "\n";
}

?>
</div>
<div>
<input type="submit" >
</div>
</form>
完了ページ

入力された値を検証し、入力されたデータに問題があればエラーを表示します。

また、入力データをテキストファイルに保存して、保存がうまくいけば完了のメッセージを表示します。

以下は「完了ページ」の検証の部分です。

まず最初に POST されたデータを全て入力値に不正なデータがないかなどをチェックする関数 checkInput() を使って検証します。

このサンプルの場合、受け取る値は全て数値であるはずなので、checkInput() では、ctype_digit() 関数を使って検証し、数値でない場合は処理を終了します。そして最後に値を integer 型に変換し、おかしな文字が入力されることを防ぎます。

その後、それぞれの POST された値に対して、数値の範囲の検証や値が配列かどうか(チェックボックスの場合)の検証をします。

そしてエラーがない場合は、入力された結果を表示します。

以下が完了ページ(questionnaire2.php)の検証と結果の表示部分のコードです。

questionnaire2.php(検証と結果の表示部分)

<h1>アンケート結果</h1>
<?php
//入力値に不正なデータがないかなどをチェックする関数
function checkInput($var){
  if(is_array($var)){
    //$var が配列の場合、checkInput()関数をそれぞれの要素について呼び出す
    return array_map('checkInput', $var);
  }else{
    //PHP 7.4.x で get_magic_quotes_gpc() は非推奨になりました
    //php.iniでmagic_quotes_gpcが「on」の場合の対策
    /*if(get_magic_quotes_gpc()){
      $var = stripslashes($var);
    }*/
    //NULLバイト攻撃対策
    if(preg_match('/\0/', $var)){
      die('不正な入力(NULLバイト)です。');
    }
    //文字エンコードのチェック
    if(!mb_check_encoding($var, 'UTF-8')){
      die('不正な文字エンコードです。');
    }
    //数値かどうかのチェック
    if(!ctype_digit($var)) {
      die('不正な入力です。');
    }
    return (int)$var;
  }
}

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

$error = 0;  //変数の初期化

//性別の入力の検証
if(isset($_POST['gender'])) {
  $gender = $_POST['gender'];
  if($gender == 1) {
    $gendername = '男性';
  }elseif($gender == 2) {
    $gendername = '女性';
  }else{
    $error = 1;  //入力エラー(値が 1 または 2 以外)
  }
}else{
  $error = 1;  //入力エラー(値が未定義)
}

//年齢の入力の検証
if(isset($_POST['age'])) {
  $age = $_POST['age'];
  if($age < 1 || $age > 8 ) {
    $error = 1;  //入力エラー(値が1-8以外)
  }
}else{
   $error = 1;  //入力エラー(値が未定義)
}

//趣味の入力の検証
if(isset($_POST['hobby'])) {
  $hobby = $_POST['hobby'];
  if(is_array($hobby)) {
    foreach($hobby as $value) {
      if($value < 0 || $value > 7) {
        $error = 1;  //入力エラー(値が0-7以外)
      }
    }
  }else{
    $error = 1;  //入力エラー(値が配列ではない)
  }
}else{
  $error = 1;  //入力エラー(値が未定義)
}

//エラーがない場合の処理(結果の表示)
if($error == 0) {
  echo '<dl>';
  echo '<dt>性別:</dt><dd>' . $gendername . '</dd>';

  //年齢の値で分岐
  if($age != 8) {
    echo '<dt>年齢:</dt><dd>' . $age . '0代</dd>';
  }else{
    echo '<dt>年齢:</dt><dd>80代以上</dd>';
  }

  //foreach で配列の数だけ繰り返し処理
  echo '<dt>趣味:</dt>';
  echo '<dd>';
  foreach($hobby as $value) {
    switch($value) {
      case 0:
        echo '音楽<br>';
        break;
      case 1:
        echo 'スポーツ<br>';
        break;
      case 2:
        echo '車<br>';
        break;
      case 3:
        echo 'アート<br>';
        break;
      case 4:
        echo '旅行<br>';
        break;
      case 5:
        echo 'カメラ<br>';
        break;
      case 6:
        echo '読書<br>';
        break;
      case 7:
        echo 'その他<br>';
        break;
    }
  }
  echo '</dd></dl>';

  //アンケート結果を保存する処理(省略→次項に記載)
 ・・・・・・

  echo '<p class="message sucess">以上の内容を保存しました。<br>アンケートにご協力いただきありがとうございました!</p>';
}else{
  echo '<p class="message error">恐れ入りますがアンケート入力ページに戻り、アンケートの項目全てにお答えください。</p>';
}
?>

もし、テキストフィールドなどの文字列の入力があるアンケートの場合は、前述の入力値に不正なデータがないかなどをチェックする関数 checkInput() では対応できないので、検証部分は以下のように checkInput() を変更し、各 POST された値を ctype_digit() 関数を使って検証するようにします。

文字列の入力があるアンケートの場合の検証の例

<?php
//入力値に不正なデータがないかなどをチェックする関数
function checkInput($var){
  if(is_array($var)){
    //$var が配列の場合、checkInput()関数をそれぞれの要素について呼び出す
    return array_map('checkInput', $var);
  }else{
    //PHP 7.4.x で get_magic_quotes_gpc() は非推奨になりました
    //php.iniでmagic_quotes_gpcが「on」の場合の対策
    /*if(get_magic_quotes_gpc()){
      $var = stripslashes($var);
    }*/
    //NULLバイト攻撃対策
    if(preg_match('/\0/', $var)){
      die('不正な入力です。');
    }
    //文字エンコードのチェック
    if(!mb_check_encoding($var, 'UTF-8')){
      die('不正な入力です。');
    }
    //改行以外の制御文字及び最大文字数のチェック
    if(preg_match('/\A[\r\n[:^cntrl:]]{0,100}\z/u', $var) === 0){
      die('不正な入力です。最大文字数は100文字です。また、制御文字は使用できません。');
    }
    return $var;
  }
}
//POSTされたデータをチェック
$_POST = checkInput($_POST);

//エスケープ処理の関数(入力された文字列を出力する際に使用)
function h($str){
  if(is_array($str)){
     //$strが配列の場合、h()関数をそれぞれの要素について呼び出す
     return array_map('h', $str);
  }else{
    if($str === null) return '';
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
  }
}

$error = 0;  //変数の初期化

//性別の入力の検証
if(isset($_POST['gender'])) {
  $gender = $_POST['gender'];
  if(ctype_digit($gender)) {
    if($gender == 1) {
      $gendername = '男性';
    }elseif($gender == 2) {
      $gendername = '女性';
    }else{
      $error = 1;  //入力エラー(値が 1 または 2 以外)
    }
  }else{
    $error = 1;  //入力エラー(値が数値ではない)
  }
}else{
  $error = 1;  //入力エラー(値が未定義)
}

//年齢の入力の検証
if(isset($_POST['age'])) {
  $age = $_POST['age'];
  if(ctype_digit($age)) {
    if($age < 1 || $age > 8 ) {
      $error = 1;  //入力エラー(値が1-8以外)
    }
  }else{
    $error = 1;  //入力エラー(値が数値ではない)
  }
}else{
   $error = 1;  //入力エラー(値が未定義)
}

//趣味の入力の検証
if(isset($_POST['hobby'])) {
  $hobby = $_POST['hobby'];
  if(is_array($hobby)) {
    foreach($hobby as $value) {
      if(ctype_digit($value)) {
        if($value < 0 || $value > 7) {
          $error = 1;  //入力エラー(値が0-7以外)
        }
      }else{
        $error = 1;  //入力エラー(値が数値ではない)
      }
    }
  }else{
    $error = 1;  //入力エラー(値が配列ではない)
  }
}else{
  $error = 1;  //入力エラー(値が未定義)
}

アンケート結果の保存

アンケート結果の保存先は、テキストファイルやデータベースが考えられますが、このサンプルではテキストファイルに保存します。

どのような仕様で保存するかを決定する必要がありますが、そのためにはアンケート結果で保存すべき項目を確認します。

このサンプルのアンケートで保存すべき項目は以下になります。

  • 性別:男性、女性
  • 年齢:10代、20代、30代、40代、50代、60代、70代、80代以上
  • 趣味:音楽、スポーツ、車、アート、旅行、カメラ、読書、その他

各項目が選択されたかどうかを、数値で保存します。上記の項目の他にアンケート回答者の総数も追加し、項目は全部で19項目になります。

  1. 男性
  2. 女性
  3. 10代
  4. 20代
  5. 30代
  6. 40代
  7. 50代
  8. 60代
  9. 70代
  10. 80代以上
  11. 音楽
  12. スポーツ
  13. アート
  14. 旅行
  15. カメラ
  16. 読書
  17. その他
  18. アンケート回答者の総数

ファイル名を log.txt として、文字コードを「UTF-8」に指定して、最初は全ての値を0にします。

log.txt ログファイル

データの保存場所

log.txt の保存場所は、ドキュメントルート(htdocs)と同じ階層(またはそれより上の階層)に「log フォルダ」を作成し、その中に保存します。

ドキュメントルート(htdocs)とは別のディレクトリに保存することで、Web ブラウザ(一般ユーザー)から直接アクセスできなくなります。もしドキュメントルート(htdocs)内のフォルダに配置してしまうと、直接ログファイルにアクセスできてしまいます。

ファイルの読み書き

fopen() 関数で、ファイル名とモード(読み込み/書き出し:r+)を指定してファイルを開きます。また、その際に 'b' フラグを指定します。(r+b)

そして、fopen()関数の戻り値を検証し、flock() 関数でファイルをロックします。

while 構文で、ファイルポインタが EOF(最後) に達するまで fgets() で各行を読み出し、読み出した値の前後のホワイトスペースを trim() 関数で除去し、配列 $results[] に格納します。

続いて、新しいアンケート結果を配列 $results[] に加算します。配列の各要素には以下の値が格納されています。

  • $results[0]  :男性が選択された回数
  • $results[1]  :女性が選択された回数
  • $results[2]  :10代が選択された回数
  • $results[3]  :20代が選択された回数
  • $results[4]  :30代が選択された回数
  • $results[5]  :40代が選択された回数
  • $results[6]  :50代が選択された回数
  • $results[7]  :60代が選択された回数
  • $results[8]  :70代が選択された回数
  • $results[9]  :80代以上が選択された回数
  • $results[10] :音楽が選択された回数
  • $results[11] :スポーツが選択された回数
  • $results[12] :車が選択された回数
  • $results[13] :アートが選択された回数
  • $results[14] :旅行が選択された回数
  • $results[15] :カメラが選択された回数
  • $results[16] :読書が選択された回数
  • $results[17] :その他が選択された回数
  • $results[18] :アンケート回答者の総数

「性別」の場合、$gender にその値が格納されているので、以下のようにすることで、結果を加算することができます。

if($gender == 1) $results[0] ++;
if($gender == 2) $results[1] ++;

「年齢」の場合も同様に以下のようにして加算することができます。

if($age == 1) $results[2] ++;
if($age == 2) $results[3] ++;
if($age == 3) $results[4] ++;
if($age == 4) $results[5] ++;
if($age == 5) $results[6] ++;
if($age == 6) $results[7] ++;
if($age == 7) $results[8] ++;
if($age == 8) $results[9] ++;

上記の変数 $age と 配列 $results[] の添え字の関係を見ると、添え字は $age に1を加えたものなので、以下のように記述することができます。

$results[$age + 1] ++;

「趣味」の場合も同様に以下のようにして加算することができますが、foreach() を使って処理すると効率よく記述できます。

if($hobby == 0) $results[10] ++;
if($hobby == 1) $results[11] ++;
if($hobby == 2) $results[12] ++;
if($hobby == 3) $results[13] ++;
if($hobby == 4) $results[14] ++;
if($hobby == 5) $results[15] ++;
if($hobby == 6) $results[16] ++;
if($hobby == 7) $results[17] ++;

$hobby の内容とチェックボックスの出力は以下のようになっています。

$hobby = array(0 => "音楽",
               1 => "スポーツ",
               2 => "車",
               3 => "アート",
               4 => "旅行",
               5 => "カメラ",
               6 => "読書",
               7 => "その他");
$ids = array('music', 'sport', 'car', 'art', 'travel', 'camera', 'book', 'other');
foreach($hobby as $key => $value) {
  echo '<input type="checkbox" name="hobby[]" value="' .$key . '>' . $value .  "\n";
}

$hobby の添え字(チェックボックスの出力では value 属性の値)に10を加えたものが、$results[] の添え字に対応するので以下のように記述できます。

foreach($hobby as $value) {
  $results[$value + 10] ++;
}

そして、アンケート回答者の総数($results[18])を加算します。

ここまでで、log.txt に記録されたアンケート結果を読み込み、新たなアンケート結果を配列 $results[] に加算しました。続いて加算されたアンケート結果を log.txt に書き込み保存します。

この時点では、ファイルポインタが EOF(最後) に達するまで fgets() で各行を読み出しているので、まず rewind() 関数でファイルポインタを先頭に戻します。

そして、加算されたアンケート結果を fwrite() 関数で、log.txt に書き込み保存し、fclose() 関数でファイルを閉じます。

以下がアンケート結果の保存部分のコードです。

questionnaire2.php(アンケート結果の保存部分)

//アンケート結果を保存するテキストファイルを指定
$textfile = '../../../log/log.txt';  //パスは環境に応じて指定

//読み込み/書き出し用にオープン (r+) 'b' フラグを指定
$fp = fopen($textfile, 'r+b');
if(!$fp) {  //fopen()関数の戻り値を検証
  exit('ファイルが存在しないか異常があります');
}
if(!flock($fp, LOCK_EX)){   //テキストを排他的にロックし、その戻り値を検証
  exit('ファイルをロックできませんでした');
}
//ファイルポインタが EOF(最後) に達するまで fgets() で各行を読み出す
while(!feof($fp)) {
  // trim() でホワイトスペースを除去して配列変数に格納
  $results[] = trim(fgets($fp));
}

if($gender == 1) $results[0] ++;
if($gender == 2) $results[1] ++;

$results[$age + 1] ++;

foreach($hobby as $value) {
  $results[$value + 10] ++;
}

$results[18] ++;

rewind($fp);

foreach($results as $value) {
  fwrite($fp, $value . "\n");
}

fclose($fp);

以下が完了ページのほぼ全文です。

questionnaire2.php

<h1>アンケート結果</h1>
<?php
//入力値に不正なデータがないかなどをチェックする関数
function checkInput($var){
  if(is_array($var)){
    //$var が配列の場合、checkInput()関数をそれぞれの要素について呼び出す
    return array_map('checkInput', $var);
  }else{
    //PHP 7.4.x で get_magic_quotes_gpc() は非推奨になりました
    //php.iniでmagic_quotes_gpcが「on」の場合の対策
    /*if(get_magic_quotes_gpc()){
      $var = stripslashes($var);
    }*/
    //NULLバイト攻撃対策
    if(preg_match('/\0/', $var)){
      die('不正な入力(NULLバイト)です。');
    }
    //文字エンコードのチェック
    if(!mb_check_encoding($var, 'UTF-8')){
      die('不正な文字エンコードです。');
    }
    //数値かどうかのチェック
    if(!ctype_digit($var)) {
      die('不正な入力です。');
    }
    return (int)$var;
  }
}

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

$error = 0;  //変数の初期化

//性別の入力の検証
if(isset($_POST['gender'])) {
  $gender = $_POST['gender'];
  if($gender == 1) {
    $gendername = '男性';
  }elseif($gender == 2) {
    $gendername = '女性';
  }else{
    $error = 1;  //入力エラー(値が 1 または 2 以外)
  }
}else{
  $error = 1;  //入力エラー(値が未定義)
}

//年齢の入力の検証
if(isset($_POST['age'])) {
  $age = $_POST['age'];
  if($age < 1 || $age > 8 ) {
    $error = 1;  //入力エラー(値が1-8以外)
  }
}else{
   $error = 1;  //入力エラー(値が未定義)
}

//趣味の入力の検証
if(isset($_POST['hobby'])) {
  $hobby = $_POST['hobby'];
  if(is_array($hobby)) {
    foreach($hobby as $value) {
      if($value < 0 || $value > 7) {
        $error = 1;  //入力エラー(値が0-7以外)
      }
    }
  }else{
    $error = 1;  //入力エラー(値が配列ではない)
  }
}else{
  $error = 1;  //入力エラー(値が未定義)
}

//エラーがない場合の処理(結果の表示)
if($error == 0) {
  echo '<dl>';
  echo '<dt>性別:</dt><dd>' . $gendername . '</dd>';

  //年齢の値で分岐
  if($age != 8) {
    echo '<dt>年齢:</dt><dd>' . $age . '0代</dd>';
  }else{
    echo '<dt>年齢:</dt><dd>80代以上</dd>';
  }

  //foreach で配列の数だけ繰り返し処理
  echo '<dt>趣味:</dt>';
  echo '<dd>';
  foreach($hobby as $value) {
    switch($value) {
      case 0:
        echo '音楽<br>';
        break;
      case 1:
        echo 'スポーツ<br>';
        break;
      case 2:
        echo '車<br>';
        break;
      case 3:
        echo 'アート<br>';
        break;
      case 4:
        echo '旅行<br>';
        break;
      case 5:
        echo 'カメラ<br>';
        break;
      case 6:
        echo '読書<br>';
        break;
      case 7:
        echo 'その他<br>';
        break;
    }
  }
  echo '</dd></dl>';

  //アンケート結果を保存するテキストファイルを指定
  $textfile = '../../../log/log.txt';

  //読み込み/書き出し用にオープン (r+) 'b' フラグを指定
  $fp = fopen($textfile, 'r+b');
  if(!$fp) {
    exit('ファイルが存在しないか異常があります');
  }
  if(!flock($fp, LOCK_EX)){
    exit('ファイルをロックできませんでした');
  }
  while(!feof($fp)) {
    $results[] = trim(fgets($fp));
  }

  if($gender == 1) $results[0] ++;
  if($gender == 2) $results[1] ++;

  $results[$age + 1] ++;

  foreach($hobby as $value) {
    $results[$value + 10] ++;
  }

  $results[18] ++;

  rewind($fp);

  foreach($results as $value) {
    fwrite($fp, $value . "\n");
  }

  fclose($fp);

  echo '<p class="message sucess">以上の内容を保存しました。<br>アンケートにご協力いただきありがとうございました!</p>';
  echo '<p class="message"><a href="questionnaire3.php">集計結果ページへ</a></p>';
}else{
  echo '<p class="message error">恐れ入りますが<a href="questionnaire1.php">アンケート入力ページ</a>に戻り、アンケートの項目全てにお答えください。</p>';
}

?>
集計ページ

完了ページでテキストファイルに保存したデータを読み取り、それぞれの項目の総数と比率を計算して表示します。

アンケート結果は、log.txt に保存されているので、完了ページと同様に fopen() 関数や fgets() で読み込んで結果を配列 $results[] に格納します。

そしてアンケート回答者の総数($results[18] の値)が0でなければ、アンケートを集計します。

比率は以下のようにして算出します。

$results[0] には男性の延べ人数が、$results[1]には女性の延べ人数が入っています。男性の比率(パーセント)は以下のようになります。

(男性の延べ人数÷ 男性と女性の総数)X 100

但し、割り算で割り切れない場合は round() 関数を使って小数点以下を四捨五入します。

$male_rate   = round($results[0] / ($results[0] + $results[1]) * 100);
$female_rate = round($results[1] / ($results[0] + $results[1]) * 100);

処理が終了したら、fclose() 関数でファイルを閉じます。

集計結果はテーブル要素に出力します。以下が集計ページのコードです。

questionnaire3.php

<?php
//アンケート結果が保存するたテキストファイルを指定
$textfile = '../../../log/log.txt';
//ファイルを開く
$fp = fopen($textfile, 'rb');   //rで読み込みモード、bで互換性維持

if(!$fp){  //fopen()関数の戻り値を検証
  exit('ファイルがないか異常があります。');
}

//テキストを排他的にロックし、その戻り値を検証
if(!flock($fp, LOCK_EX)){
  exit('ファイルをロックできませんでした。');
}

//ファイルポインタが EOF(最後)に達するまで、テキストの各行を読み出し、trim()関数で文字列の先頭および末尾にあるホワイトスペースを取り除き配列に格納
while(!feof($fp)){
  $results[] = trim(fgets($fp));
}

if($results[18] != 0){  //アンケート結果が0でなければ集計
  echo '<p>アンケートの集計結果:総数 ' . $results[18] . ' 件</p>';

?>

<table>
  <thead>
  <tr>
  <th>質問</th>
  <th>人数</th>
  <th>比率</th>
  </tr>
  </thead>
  <tbody>
  <tr>
  <td>性別</td>
<?php
  // 男女の比率計算
  $male_rate   = round($results[0] / ($results[0] + $results[1]) * 100);
  $female_rate = round($results[1] / ($results[0] + $results[1]) * 100);

  echo '  <td>男性:' . $results[0] . '人 女性:' . $results[1] . '人</td>';
  echo '  <td>男性:' . $male_rate . '% 女性:' . $female_rate . '%</td>';
?>
  </tr>
  <tr>
  <td>年齢</td>
<?php
  $total_age = 0;
  for($x = 2; $x <= 9; $x++) {
    $total_age += $results[$x];
  }
  $age10_rate = round($results[2] / $total_age * 100);
  $age20_rate = round($results[3] / $total_age * 100);
  $age30_rate = round($results[4] / $total_age * 100);
  $age40_rate = round($results[5] / $total_age * 100);
  $age50_rate = round($results[6] / $total_age * 100);
  $age60_rate = round($results[7] / $total_age * 100);
  $age70_rate = round($results[8] / $total_age * 100);
  $age80_rate = round($results[9] / $total_age * 100);

  echo '  <td>10代:' . $results[2] . '人<br>' .
             '20代:' . $results[3] . '人<br>' .
             '30代:' . $results[4] . '人<br>' .
             '40代:' . $results[5] . '人<br>' .
             '50代:' . $results[6] . '人<br>' .
             '60代:' . $results[7] . '人<br>' .
             '70代:' . $results[8] . '人<br>' .
         '80代以上:' . $results[9] . '人</td>';
  echo '  <td>10代:' . $age10_rate . '%<br>' .
             '20代:' . $age20_rate . '%<br>' .
             '30代:' . $age30_rate . '%<br>' .
             '40代:' . $age40_rate . '%<br>' .
             '50代:' . $age50_rate . '%<br>' .
             '60代:' . $age60_rate . '%<br>' .
             '70代:' . $age70_rate . '%<br>' .
         '80代以上:' . $age80_rate . '%</td>';
?>
  </tr>
  <tr>
  <td>趣味</td>

<?php
  $total_hobby = 0;
  for($x = 10; $x <= 17; $x++) {
    $total_hobby += $results[$x];
  }
  $hobby1_rate = round($results[10] /$total_hobby * 100);
  $hobby2_rate = round($results[11] /$total_hobby * 100);
  $hobby3_rate = round($results[12] /$total_hobby * 100);
  $hobby4_rate = round($results[13] /$total_hobby * 100);
  $hobby5_rate = round($results[14] /$total_hobby * 100);
  $hobby6_rate = round($results[15] /$total_hobby * 100);
  $hobby7_rate = round($results[16] /$total_hobby * 100);
  $hobby8_rate = round($results[17] /$total_hobby * 100);

  echo '  <td>音楽:' . $results[10] . '人<br>' .
         'スポーツ:' . $results[11] . '人<br>' .
               '車:' . $results[12] . '人<br>' .
           'アート:' . $results[13] . '人<br>' .
             '旅行:' . $results[14] . '人<br>' .
           'カメラ:' . $results[15] . '人<br>' .
             '読書:' . $results[16] . '人<br>' .
           'その他:' . $results[17] . '人</td>';
  echo '  <td>音楽:' . $hobby1_rate . '%<br>' .
         'スポーツ:' . $hobby2_rate . '%<br>' .
               '車:' . $hobby3_rate . '%<br>' .
           'アート:' . $hobby4_rate . '%<br>' .
             '旅行:' . $hobby5_rate . '%<br>' .
           'カメラ:' . $hobby6_rate . '%<br>' .
             '読書:' . $hobby7_rate . '%<br>' .
           'その他:' . $hobby8_rate . '%</td>';
?>
  </tr>
  </tbody>
  </table>
<?php
} else {
  // アンケートデータがない場合
  echo '  <p class="msg">表示できるようなアンケートデータがありません。</p>';
}
fclose($fp);
echo '<p class="link"><a href="questionnaire1.php">アンケートページへ戻る</a></p>';
?>
jQuery を使った検証を追加

前述のサンプルの場合、ユーザーが全ての項目を入力しないでアンケートを送信した場合、「恐れ入りますがアンケート入力ページに戻り、アンケートの項目全てにお答えください。」という表示がされユーザーはアンケートの入力ページに戻らなければならず、またどの項目が未記入なのかが表示されず、あまり親切ではありません。

そこで、アンケートの入力ページに、jQuery を使った検証を追加してみます。

HTML 部分はほぼ同じですが、以下のラジオボタンとチェックボックスの部分のみ少し変更してあります。

これは、jQuery の検証対象とするために、class 属性を追加するためです。

<input type="radio" name="gender" id="male" value="1" class="required">
・・・中略・・・
foreach($hobby as $key => $value) {
  if($key == 0) {
    echo '<label for="music"><input type="checkbox" name="hobby[]" value="0" id="music" class="required">' . $value . '</label>' . "\n";
  }else{
    echo '<label for="' . $ids[$key] .'"><input type="checkbox" name="hobby[]" value="'
    .$key . '" id="' . $ids[$key] . '">' . $value . '</label>' . "\n";
  }
}

また、エラー表示の際の以下のような CSS を設定します。

.error input ,
.error select ,{
    background-color: #F8DFDF;
}
p.error{
    margin:0;
    color:red;
    font-weight:bold;
    margin-bottom:1em;
}

個々の検証については「jQuery フォームのバリデーション(検証)」を参照ください。

questionnaire1.php with jQuery validation

<h1>アンケート入力ページ</h1>

<form action="questionnaire2.php" method="post">
<div>
<p>性別</p>
  <input type="radio" name="gender" id="male" value="1" class="required">
    <label for="male"> 男性 </label>
  <input type="radio" name="gender" id="female"  value="2">
    <label for="female"> 女性 </label>
</div>
<div>
<p><label for="age"> 年齢 </label></p>
<select name="age" id="age">
<option value="0" selected>選択してください。</option>
<?php
for($num = 1; $num <= 7; $num++) {
  echo '<option value="' . $num . '">' . $num . '0代</option>' . "\n";
}
?>
<option value="8">80代以上</option>
</select>
</div>
<div>
<p>趣味</p>
<?php
$hobby = array(0 => "音楽",
               1 => "スポーツ",
               2 => "車",
               3 => "アート",
               4 => "旅行",
               5 => "カメラ",
               6 => "読書",
               7 => "その他");
$ids = array('music', 'sport', 'car', 'art', 'travel', 'camera', 'book', 'other');

foreach($hobby as $key => $value) {
  if($key == 0) {
    echo '<label for="music"><input type="checkbox" name="hobby[]" value="0" id="music" class="required">' . $value . '</label>' . "\n";
  }else{
    echo '<label for="' . $ids[$key] .'"><input type="checkbox" name="hobby[]" value="'
    .$key . '" id="' . $ids[$key] . '">' . $value . '</label>' . "\n";
  }
}

?>
</div>
<div>
<input type="submit" >
</div>
</form>

<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<script type="text/javascript">
jQuery(function($){
  $("form").submit(function(){
    //エラー表示の初期化
    $("p.error").remove();
    $("div").removeClass("error");
    var text = "";

     //ラジオボタンの検証
    $(":radio").filter(".required").each(function(){
      if($('input[name="'+$(this).attr("name")+'"]:checked').size() == 0){
        text = $(this).parent().find('p').text();
        $(this).parent().prepend("<p class='error'>" + text + "を選択してください。</p>");
      }
    })

    //セレクトメニューの検証
    $("select").each(function(){
       if($(this).val() == 0 ) {
         text = $(this).parent().find('label').text();
         $(this).parent().prepend("<p class='error'>" + text + "を選択してください。</p>");
       }
    });

    //チェックボックスの検証
    $(":checkbox").filter(".required").each(function(){
      if($('input[name="'+$(this).attr("name")+'"]:checked').size() == 0){
        text = $(this).closest('div').find('p').text();
        $(this).parent().prepend("<p class='error'>" + text + "を選択してください。</p>");
      }
    })

    //error クラスの追加の処理
    if($("p.error").size() > 0){
      $("p.error").parent().addClass("error");
      return false;
    }

  })

});
</script>