Home > 未分類

未分類 Archive

書置き

しばらく山の方にいってきます。探さないで下さい。

PHP を頑張るかあさんへ

PHP で入力フォームのエラーチェック – p15.jp で riszw かあさんが PHP にチャレンジしており、ソースコードの良し悪しなんてのは相対的なものなので一概には言えませんが、それでも効率化の面において「もっとよくなるよ」と色々 IRC で突っ込んでいたのですが、書ききれなくなってきたのでエントリ。

まだ仕事でプログラムを組み始めて2年くらいのぺーぺーなんで「ここをこうするともっといいのに」という点が多々あるかと思いますが、その辺り見つけられたら突っ込んで頂ければ幸いです。

PHP4 はもうすぐさようならなので PHP5 を前提にお話します。
それでは上から順に。

// チェックリスト
$data = array(
$_POST["changeColor"] => ‘/^[0-9a-f]{6}$/i’,
$_POST["changeWidth"] => ‘/^[0-9]{1,2}$/’,
$_POST["changeLineheight"] => ‘/^[0-9.]{1,3}$/’
);
?>

配列のキーは決めうちで

配列のキーにクライアントからの値を渡すのは好ましくありません。
なぜならば post された値が重複していればキーが重複することになり、重複したキーの値は最後に定義された値のみ適用されるからです。
それ以外にも配列のキーとしては使えるけど変数名としては使えない文字(スペースなど)が含まれていた場合、配列を extract() で変数として扱う事も出来ません。

ユニコードの正規表現はパターン修飾子に u を指定

また、配列の値に正規表現のクエリを指定していますが、UTF-8 などユニコード文字列が渡される場合 preg_match() などの PCRE 関数ではパターン修飾子に i だけではなく u も指定する必要があります。
正規表現に限らず PHP の場合は組み込みの関数に文字コードを教えてあげるケースが多々あります。

例えば mb_encode_mimeheader() は呼び出す前に mb_internal_encoding() でエンコードしたい文字コードを指定しておかなければならないなど、マニュアルには載ってない(マニュアルのコメントには書かれていますが)仕様(バグ?)があったりします。

[参考]
PHP: 正規表現パターンに使用可能な修飾子 – Manual

改善案

<?php
// チェックリスト
$data = array(
    array( 'value' => $_POST["changeColor"], 'query' => '/^[0-9a-f]{6}$/iu' ),
    array( 'value' => $_POST["changeWidth"], 'query' => '/^[0-9]{1,2}$/u' ),
    array( 'value' => $_POST["changeLineheight"], 'query' => '/^[0-9.]{1,3}$/u' ),
);
?>

次のセクション。

// フォームから送信されたものか確認
if(isset($_POST['submit'])) {

// チェックリストを繰り返す
foreach($data as $post => $regex) {

// 正規表現に当てはまらない場合
if(!preg_match($regex, $post)) {

// 未入力があった場合
if($post == “”) {
$errorMsg = “未入力の項目がありますのでご確認ください。”;
}

// 入力値が不正な場合
else {
$errorMsg = “「 {$post} 」 と入力されている部分がエラーとなっていますのでご確認ください。”;
}

// エラーメッセージを出力
echo $errorHeader, $errorMsg, $errorFooter;

// 終わり
exit();
}
}

// チェックリストの繰り返しの終わり
unset($data);
}

// フォームから送信されたものでない場合 ( output.php を直接開いたとき)
else {
$errorMsg = “<a href=”http://p15.jp/files/study/2008/05/input.html”>http://p15.jp/files/study/2008/05/input.html</a> から送信してください。”;
echo $errorHeader, $errorMsg, $errorFooter;
exit();
}

条件分岐は必要最低限に

まず if(isset($_POST['submit'])) はエラーチェックなので if(isset($_POST['submit'])){正常}else{エラー} という処理ではなく if(!isset($_POST['submit'])){エラー} と言う処理だけにした方がコードの見通しが良くなります。
○○の時だけ、ならばそうでないときは条件分岐に含める必要はありません。
無駄にネスト(条件分岐や繰り返し処理の入れ子構造)が増えてしまうとコードの内容を目で追う事が難しくなってきてしまいます。

改善案

<?php
if( !isset( $_POST['submit'] ) ) {
    $errorMsg = "<a href="http://p15.jp/files/study/2008/05/input.html">http://p15.jp/files/study/2008/05/input.html&lt;/a> から送信してください。";
    echo $errorHeader, $errorMsg, $errorFooter;
    exit();
}
?>

と、ここまで書いておいてアレですが根幹的なところで「大きなプログラムを書いたときに困りそう」な書き方だったので、全ソースを引用した上で改善案を出します。

// チェックリスト
$data = array(
$_POST["changeColor"] => ‘/^[0-9a-f]{6}$/i’,
$_POST["changeWidth"] => ‘/^[0-9]{1,2}$/’,
$_POST["changeLineheight"] => ‘/^[0-9.]{1,3}$/’
);

// エラーのときの XHTML (ヘッダ)
$errorHeader = “<?xml version=”1.0″ encoding=”UTF-8″ ?>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Strict//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd”>
<html xmlns=”http://www.w3.org/1999/xhtml” xml:lang=”ja” lang=”ja”>
<head profile=”http://purl.org/net/ns/metaprof”>
<meta http-equiv=”Content-Type” content=”text/html; charset=UTF-8″ />
<title>
エラーが発生しました – JavaScript のおべんきょう 2008 年 5 月吉日 から送信されたデータ
</title>
</head>

<body>

<h1>
エラーが発生しました
</h1>
<p>”;

// エラーのときの XHTML (フッタ)
$errorFooter = “</p>
</body>
</html>”;

// フォームから送信されたものか確認
if(isset($_POST['submit'])) {

// チェックリストを繰り返す
foreach($data as $post => $regex) {

// 正規表現に当てはまらない場合
if(!preg_match($regex, $post)) {

// 未入力があった場合
if($post == “”) {
$errorMsg = “未入力の項目がありますのでご確認ください。”;
}

// 入力値が不正な場合
else {
$errorMsg = “「 {$post} 」 と入力されている部分がエラーとなっていますのでご確認ください。”;
}

// エラーメッセージを出力
echo $errorHeader, $errorMsg, $errorFooter;

// 終わり
exit();
}
}

// チェックリストの繰り返しの終わり
unset($data);
}

// フォームから送信されたものでない場合 ( output.php を直接開いたとき)
else {
$errorMsg = “<a href=”http://p15.jp/files/study/2008/05/input.html”>http://p15.jp/files/study/2008/05/input.html</a> から送信してください。”;
echo $errorHeader, $errorMsg, $errorFooter;
exit();
}

ソースコードと HTML を混在させない

PHP に限らずソースコードと HTML を混在させるのは HTML でビジュアルデザインを作ってしまうのと同じで「わかりにくい」スクリプトとなってしまいます。
また、新たな機能を実装しようとした場合、どこにコーディングしていいのかわからなくなってしまうなど、運用の面において余りよろしくありません。
プログラムの規模が大きくなってくると Smarty などいわゆるテンプレートエンジンと言うライブラリを使用してほぼ完全にソースコードと HTML を分離するのですが、小規模なプログラムにおいてテンプレートエンジンを採用するのはケースにもよりますが「消費するリソース>受ける恩恵」となりがちですので、「ある程度の分離」が出来ればよいと思います。
また、分離の定義自体主観によって変わると思いますので、規模の小さいプログラムで物理的に別ファイルにする事は「消費するリソース>受ける恩恵」がモロに当てはまったりするのでやはり「自分がメンテしやすい程度に分離できている」位の判断でよいと思います。

エラー処理をたくさん書かない

エラー処理が散在しているとエラー処理の実装を修正したいときなどにとっても不便です。
エラーが発生したときに管理者にメールを送っておきたいなー。なんて思いが浮かんでも実装が面倒です。
また、エラー処理を個々に書くと場所場所で実装が変わったり、そもそも実装が間違っていて実は動かないけどエラー発生させてチェックしてないから気づいてない、何ていう事も減ります。もちろんエラーを発生させてチェックしないのはアウトですが、同じ実装を何度も書くのは「リソースの無駄」「タイプミスの可能性」「面倒くさい」とどこにもメリットがありません。

改善案

<?php
// チェックリスト
$data = array(
    array( 'value' => $_POST["changeColor"], 'query' => '/^[0-9a-f]{6}$/iu' ),
    array( 'value' => $_POST["changeWidth"], 'query' => '/^[0-9]{1,2}$/u' ),
    array( 'value' => $_POST["changeLineheight"], 'query' => '/^[0-9.]{1,3}$/u' ),
);
 
try {
    /** 送信ボタンが押されなかった場合 */
    if( !isset( $_POST['submit'] ) )
        throw new Exception( '<a href="http://p15.jp/files/study/2008/05/input.html">http://p15.jp/files/study/2008/05/input.html&lt;/a> から送信してください。' );
 
    // チェックリストを繰り返す
    foreach( $data as $arg ) {
 
        // 未入力があった場合
        if( empty( $arg['value'] ) )
            throw new Exception( '未入力の項目がありますのでご確認ください。' );
 
        // 正規表現に当てはまらない場合
        if( !preg_match( $arg['query'], $arg['value'] ) )
            throw new Exception( "{$arg['value']} 」 と入力されている部分がエラーとなっていますのでご確認ください。" );
 
    }
    
    // チェックリストの繰り返しの終わり
    unset($data);
    
} catch( Exception $e ){
/** エラー処理 */
echo '<?xml version="1.0" encoding="UTF-8" ?>';
?>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head profile="http://purl.org/net/ns/metaprof">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8″ />
    <title>
        エラーが発生しました - JavaScript のおべんきょう 2008 年 5 月吉日 から送信されたデータ
    </title>
</head>
 
<body>
 
<h1>
    エラーが発生しました
</h1>
<p>
    <?php echo htmlentities( $e->getMessage(), ENT_QUOTES, 'UTF-8' ); ?>
</p>
</body>
</html>
<?php
}
?>

解説

いきなりわけのわからない形になったと思いますので、順を追って解説します。

try{}catch( Exception $e ){} はエラー処理を分離する魔法

try{~}catch(~){~} は乱暴に言ってしまえば「try{} の中で throw new hogehoge() が呼び出されたら catch( hogehoge $foo ){} を実行しますよ!」という意味です。
すなわち、try{} の中で throw new Exeption( ‘hugahuga’ ) が呼び出されたら catch( Exception $foo ){} に処理が渡されると言う事です。

throw new hogehoge() の hogehoge は何でもいいと言うわけではなく、組み込みの Exception クラスか Exception クラスを継承したクラスである必要があります。
よくわからない><と言うのであれば改善案のように Exception クラスを指定すればよいでしょう。

組み込みの Exception クラスには多くのメソッドがありますが、catch( Exception $e ){} の中で使用している $e->getMessage() は Exception( ここの文字列が表示される ) と言う働きをするメソッドです。今回はこのメソッドしか使わないのでこれだけ覚えておけば OK です。

さて、try{}catch(){} を使うことで実際にどう変わったのか見てみましょう。

<?php
/** 送信ボタンが押されなかった場合 */
if( !isset( $_POST['submit'] ) )
        throw new Exception( '&lt;a href="http://p15.jp/files/study/2008/05/input.html">http://p15.jp/files/study/2008/05/input.html&lt;/a> から送信してください。' );
?>
<?php
// 未入力があった場合
if( empty( $arg['value'] ) )
    throw new Exception( '未入力の項目がありますのでご確認ください。' );
?>
<?php
// 正規表現に当てはまらない場合
if( !preg_match( $arg['query'], $arg['value'] ) )
    throw new Exception( "{$arg['value']} 」 と入力されている部分がエラーとなっていますのでご確認ください。" );
?>

これらは全てエラーがあるかどうかを判断していますが、「エラーがあったときの処理」はしていません。
エラーがあったときの処理は catch( Exception $e ){} に書かれているため一箇所でしか実装していません。全てのエラーはこの処理をされるわけです。
同時にエラーの処理が一箇所になったため、「エラーが発生したら exit でスクリプト停止しなきゃ><」と言うこともなくなりました。
try{}catch(){} で一度 throw されたらもう try{} に戻ってくる事はありません。

さらに同時に HTML が catch(){} の中に入ったので、ソースコードの邪魔になりません。
「あれ、この HTML よりも先にこの処理しないといけなかったんだっけ?」みたいな事もなくなったわけです。

JavaScript の時も指摘しましたが、似たようなコードを何度も書かなければならないならそれはプログラムではありません。Excel のシート関数です。

HTML として表示したいときは表示する内容を制御する

これは HTML に限らずプログラムでメールを生成するときなどにも言えるのですが、クライアントからリクエスト(入力)された文字列を「どのような文字列であるか」と予測する事は出来ません。出来たらエスパーですね。
要するに作り手が「このテキストフィールドには名前を入力する欄だから、名前しか入ってこないだろうな」と考えるのは「本当に悪い人はいないから」と言って夜中のスラム街をすっぽんぽんで歩くのと変わりません。

HTML の場合 HTML の構造を形作る文字があります。「<,>,/」などですね。これらの文字がクライアントからの文字列に含まれていた場合、その文字列はただの文字列ではなく HTML として文書構造を定義してしまう可能性があります。ほとんどの場合このような形でクライアントが任意に HTML を書けてしまうのは危険と言えます。
例えばフォームの入力に不備があった場合、入力してもらった内容をフォームのそれぞれの要素に入力した形で再度フォームを表示するようなプログラムがあったとします。

<form action="" method="post">
	<input type="text" name="mailaddress" value="" />
	<input type="submit" name="SUBMIT" value="送信" />
</form>

と言うような HTML を表示するのに

<?php
echo "<form action=\"\" method=\"post\">
    <input type=\"text\" name=\"mailaddress\" value=\"{$_POST['mailaddress']}\" />
    <input type=\"submit\" name=\"SUBMIT\" value=\"送信\" />
</form>"
?>

と言うようなプログラムがあったとします。
ブラウザで開けば、mailaddress テキストフィールドにはブラウザで入力できる文字であるなら何でも入力できる状態です。
さらにフォームの表示をするために $_POST['mailaddress'] をそのまま指定しています。
と言う事は「“></form>」と入力してフォーム要素を閉じる事も「“>&lt/body>&lt/html>」と入力して HTML を終了させる事も可能です。
” onfocus=”location.href=’http://yahoo.co.jp’」と入力したら input 要素にフォーカスしたとたん Yahoo! に移動してしまう事になります。
いわゆる「スクリプトの埋め込み」を可能にしてしまうこのような状況は様々な悪さの足がかりとなるケースが多々あります。
様々な悪さについては割愛しますが、要するに「出力先の仕様に合わせて出力する内容を制御する」と言うのがとても大事なのです。

上記の例であれば以下のようにする事で HTML の構造を定義できる文字を実体参照に置き換える事となり、HTML の構造が変わってしまう事はありません。

<?php
echo "<form action=\"\" method=\"post\">
    <input type=\"text\" name=\"mailaddress\" value=\"".htmlentities( $_POST['mailaddress'], ENT_QUOTES, 'UTF-8' )."\" />
    <input type=\"submit\" name=\"SUBMIT\" value=\"送信\" />
</form>"
?>

ついでに言うとこの htmlentities() も渡された文字列の文字コードを指定してあげる必要があります。
めんどっちいですね。

どうでしょう。
コンピュータに仕事させるための準備から楽をしなければプログラムから享けられる恩恵は微々たる物だと考えています。
楽をするための努力は往々にして楽しいものです。だからこそ楽(らく)と楽しいは同じ漢字なのかもしれません。
楽しく楽をするために頑張って下さい。

以上、現場から生中継でお送りしました。

ホーム > 未分類

Search
Feeds
Meta

Return to page top