Lesson 49 バリデーションを理解して設定しましょう

バリデーションとは受け付けない値を弾いて、確かな(valid)値だけを扱うという処理です。 こう説明されてもわかりにくいと思うので、いつもの生活の中でどのようなチェックが行われているかを例に説明しましょう。 アンケートフォームなどで数字の項目に文字を入れるとエラーになったり、携帯電話で着信拒否の番号を登録したり、身近で値のチェックを行っていますね。 本書で作成したレシピアプリでも、POSTやGETで値を取得していますが、このような値はHTML5やJavaScriptなどで入力チェックを行っていても信用してはいけません。ここで重要になるのがバリデーションです。

※Windows環境の場合は、使用しているエディタによってはプログラム内の「\」(半角バックスラッシュ)が「¥」(半角円マーク)で表示される場合があります。

チェックすべき項目

1. 文字種(文字列か数字かなど)
2. 桁数、文字数(何桁までか、何文字までかなど)
3. フォーマット(いくつ以上、いくつ未満、電話番号やメールアドレスのように形が決まっているなど)

これらを調べて、不正な入力値を除外する必要があります。

バリデーションに便利なpreg_match

バリデーションに適した関数を紹介しましょう。 preg_matchという関数です。 preg_match()は正規表現により、値が期待するパターンにマッチするかを検査します。

preg_match(パターン,検査する文字列);
    パターンにマッチすると1
    パターンにマッチしないと0
    エラーの場合falseを返します。

正規表現というと難しいですが、ここでは下記のサンプルを見て雰囲気をつかんでください。

一桁の整数で1か2か3

preg_match('/\A[123]\z/',変数);

4桁の整数

preg_match('/\A[0-9]{1,4}+\z/',変数);

100文字以内の文字列

preg_match('/\A[[:^cntrl:]]{1,100}+\z/u',変数);

改行のある200文字以内の文字列

preg_match('/\A[\r\n[:^cntrl:]]{1,200}+\z/u',変数);

なんとなく予想がつきませんか? パターンの部分で「/\A ... \z/」というふうに、「/」に挟まれた中にそれぞれ違いがあります。 ここでは細かく説明しませんが、この書き方を覚えておいてください。

表:例に使われている正規表現の簡単な説明

項目 内容
// //で囲まれた部分に正規表現が入っている
\A 文字列の先頭
\z 文字列の最後
[] []の中が使って良い文字列になる
0-9 0〜9の数字
a-z アルファベット
[:^cntrl:] 制御文字以外
\r\n 改行
+ 一文字以上の繰り返し
{} {}の直前のパターンの有効桁(文字)数
u UTF-8が対象

さらに詳細な内容ついては「PHP 正規表現」などで検索し、どのようなものなのか勉強してください
(コピー&ペーストでの利用は意図しない処理になる場合もあるので、よく内容を理解して利用しましょう)。 この先では上記サンプルのみを用います。

一歩進んだif文での判定

さて、バリデーションをするためには、色々な条件で入力を受け付けない文字や数値を設定する必要があります。 そのようなときにif文を用いて複数の条件を1回で判定する方法をここで補足します。 まずif文の構文を見てみましょう。

if (条件) {
}

でしたね。この条件というところがポイントで、中が真(TRUE),偽(FALSE)かで条件の判定を行います。 elseも覚えましたが、ここでは条件の部分に注目して新しい使い方を覚えましょう。

否定

「●●ではない」というような条件を利用したい場合は多いと思います。そのような場合は論理演算子の!を利用することで条件の否定を行うことができます。

例:emptyではない場合

if (!empty($a)) {
    echo 'emptyではありません。';
}
 
if ( $a !== 'blue') {
    echo '$aはblueではありません。';
}

例を見てみると何か気づくと思いますが、 if文の条件で!(否定)を付けることはelseの条件のときと同じです。 真(TRUE)の側の判定が不要な場合、!を用いるとif文が短くなり可読性がよくなるので覚えておきましょう。

例1をif〜elseで表すと

if (empty($a)) {
    echo 'emptyです。'; // <===今回は不要
} else {
    echo 'emptyではありません。'; // <==ここだけを使いたい
}

条件文でよく出てくるので覚えておくといいでしょう。

複合条件

いままではシンプルな判定を行ってきましたが複合的な判定も必要になります。 例えば「●●かつ●●」や、「●●または●●」といった判定です。

「かつ」の場合は論理積と呼ばれるもので、2つの条件を&&でつなぎます。 「または」の場合は論理和と呼ばれ、2つの条件を||でつなぎます。 なお、複数の条件を利用する場合、1つ1つの条件を()で括ることで可読性が増します。

例:

if (($color === "blue") && ($flower === 'バラ')) {
    echo '青いバラです。';
}
 
if (($prefecture === '東京都') || ($prefecture === '神奈川県')) {
    echo '東京都か神奈川県です。';
}

この後も利用するので覚えておいてください。

emptyとisset

emptyとは逆に、存在するということを判定するissetという言語構造があります。 先ほど!(否定)を学びました。この2つは完璧に対になるものではないので注意が必要です。 大きな違いは0の判定を行ったときに0を空とするのか、数値が入力されているのかどちらで判定するかです。

<?PHP
$a = 0;
var_dump(empty($a));
var_dump(!isset($a));

このような短いサンプルをtest.phpなどとして実行してみましょう。 結果は 「int(0) bool(true) bool(false)」 と、「empty($a)」は「true」、「!isset($a)」は「false」と、異なる判定結果が返ってきます。empty()とisset()が完璧に対の関係ではないということが理解できたでしょうか?

※他にも「$a="";」の場合などでも異なる判定結果が返ります。

レシピアプリではIDはデータベースのオートインクリメント設定によって、1から始まりました。通し番号では0から始まる場合もありますね。このような場合、上記のようにif (empty($id)) と記述してしまうと、0も条件を満たしたと判断されてしまいます。これはempty()が0を真(TRUE)と判定するためです。このような場合は、値がある場合に真(TRUE)と判定するisset()という関数を用いましょう。isset()は0が入力されても入力があると判定します。このisset()に先ほど説明に出てきた!(否定)を行うと何も入力されていない場合の判定が行えます。なお、0や未入力の扱いには注意が必要です。マニュアルを参考にテストを行い意図したデータを判定しているか確認しましょう。

PHPマニュアルなどを参考にして、仕様上どちらが最適かを検討してください。
http://php.net/manual/ja/function.isset.php
http://php.net/manual/ja/function.empty.php

IDのバリデーションを行う

いよいよ実際にバリデーションを行っていきましょう。 まずはIDの場合を考えてみます。

いまIDの値はどのように作成されるかというと、 データベースを作成したときのオートインクリメントが有効になっているので、 1から順にカウントアップしていきます。 PHPにもMySQLにも扱える型により扱える限界はあります。 ※執筆時点でのMAMPの環境では、MySQLのINT型の範囲-2147483648〜2147483647、PHPのinteger型の最大値9223372036854775807(脚注) いままで決めていませんでしたが、今回のプログラムでは1000件まで入力できることに決めましょう。 とすると、IDに入力されるべき整数は1-1000の整数ということになります。 では、それ以外の値についても下記表のように、入力できる値に制約を設けましょう。

表:入力を許可する値

項目 内容
ID 1から1000までの整数
料理名 100文字までの文字列
カテゴリ、難易度 1〜3の整数
予算 0-9999までの整数
作り方 200文字までの文字列と改行

ではそれぞれの項目についてソースコードを修正していきましょう。 レシピの数は1000個まで登録できるようにしましょう。

対象となるファイルはdetail.php、edit.php、delete.phpになります。 それぞれの箇所から該当する箇所を探して修正しましょう。

元の箇所

if (empty($_GET['id'])) throw new Exception('ID不正');
$id = (int) $_GET['id'];

変更後

if (!isset($_GET['id'])) throw new Exception('ID無し');
if (!preg_match('/\A[0-9]{1,4}+\z/',$_GET['id'])) throw new Exception('ID不正');
$id = (int)$_GET['id'];
if (($id < 1) || ($id) > 1000 ) throw new Exception('ID範囲外');
  1. emptyでの判定だと0を入力しているのに「ID無し」のエラーになっています。0はIDがないのではなく範囲外ということなので、はじめのemptyを!issetに変更しましょう。
  2. 次はpreg_matchのパターンを使って整数の4桁の判定を行います。
  3. 値の範囲を決めてif文で判定しましょう。ここでは、値は1より小さいか1000より大きいという2つの条件に当てはまらない場合にExceptioをthrowします。またここで追加する箇所は$id = (int) $_GET['ind']の後です。$idに数値型キャストを行ってから、$idを利用して数値の大小比較を行います。
  4. いままでExceptionの後にはID不正という汎用的なメッセージを指定していましたが、これからはそれぞれのエラー適したエラーメッセージを表示するようにしましょう。

さてURLにIDが付与されてくるのは、いま修正した3つのファイルでしたが、 もう1カ所IDを受け渡す処理がありましたね。 更新作業の送信をした後にデータベースに書き込むupdate.phpです。 update.phpにも同様の処理を入れておきましょう。 該当する行を見てみると違いはGETとPOSTだけですね。同様に修正しておきましょう。

if (!isset($_POST['id'])) throw new Exception('ID無し');
if (!preg_match('/\A[0-9]{1,4}+\z/',$_POST['budget'])) throw new Exception('ID不正');

では、修正のテストしてみましょう。以下のテスト項目を満たしているか試してみましょう。

  1. 新規追加が正しくできているか
  2. 変更が正しくできているか
  3. detail.phpの引数にid=0,id=あ,id=10000,id= などと入力してErrorが表示されるか

どうでしょう。追加、更新に問題は無かったですか?メッセージは正しく表示されましたか? 処理が上手くいかない場合はもう一度ソースコード見直しましょう。 エラーメッセージの内容をよく読んで、どこか原因か探りましょう。

detail.php、edit.php、delete.php共に修正して、index.phpからそれぞれの処理が正しく動作することを確認してください。

ID以外の項目のバリデーションを行う

次は新規追加と更新の修正を行います。 つまりadd.phpとupdate.phpを修正します。

新規追加と更新の際には、ID以外の項目のバリデーションも行う必要があります。

ID以外の項目のバリデーションはadd.phpとupdate.phpで共通する部分が多いので、 先ほど覚えたinclude_onceを利用しましょう。 db_config.phpと同じ階層にerror_check.phpを作成しましょう。

<?php
if (!preg_match('/\A[[:^cntrl:]]{1,100}+\z/u',$_POST['recipe_name'])) {
    throw new Exception('料理名を正しく入力してください。');
}
if (!preg_match('/\A[123]\z/',$_POST['category'])) {
    throw new Exception('カテゴリを正しく入力してください。');
}
if (!preg_match('/\A[123]\z/',$_POST['difficulty'])) {
    throw new Exception('難易度を正しく入力してください。');
}
if (!preg_match('/\A[0-9]{1,4}+\z/',$_POST['budget'])) {
    throw new Exception('予算を正しく入力してください。');
}
if (!preg_match('/\A[\r\n[:^cntrl:]]{1,200}+\z/u',$_POST['howto'])) {
    throw new Exception('作り方を正しく入力してください。');
}
?>

いままではif文の後に{}で括らずに、すぐにthrow new Exceptionを記述していましたが、 判定やメッセージなど1行で表示するには長くなってきたので、 if{}の書き方にしましょう(いままでは誌面の節約のために1行の記述が多かったのですが、今後は{}を使うようにしましょう)。

それぞれのpreg_matchによる判定で利用している正規化表現はこのレッスンの冒頭で説明したものばかりです。内容を見比べてください。 POSTされたそれぞれの変数と比べてマッチしないもの(!を先頭に付ける)でExceptionを発生させます。 Exceptionの引数のエラーメッセージもエラーの内容がわかるものにしましょう。

後は実際にadd.phpとupdate.phpに、このファイルを読み込むだけです。db_config.phpの読み込み箇所を参考に追加してみましょう。

追加する箇所はtryのすぐ後で、項目ごとに変数に格納する前です。

include_once '/Applications/MAMP/error_check.php';(Macの場合)
 
include_once '\MAMP\error_check.php';(Windowsの場合)

※includeできない場合はファイル名で検索して、保存場所のパスを指定してください。

これでadd.phpとupdate.phpの修正は完了です。 includeさせると、そのファイルの中身がプログラムに取り込まれます。 これでadd.phpやupdate.phpが実行されるたびに毎回、いま作成したバリデーション処理が実行されることになります。

それでは、実際に予算に9999より大きな数字を入れてみて、メッセージを確認してみましょう。 エラーメッセージが表示されましたか? これでバリデーションは完成です。 お疲れ様でした!

バリデーションは仕様に沿った入力を行うために必要になります。 仕組みを覚えておいて、ぜひ活用してください。