実践PHPの部屋

HOME

■ はじめに

ここ『実践PHPの部屋』では、とにかく実践的なことを書き連ねていきます。

ただし、大規模開発は眼中にないのでファイルをいくつも使ったモジュール開発ではなく、一つのファイルだけで完結するようなチョットしたスクリプトを念頭に置いています。

この部屋で実践的かどうかを決めているのはわたしの偏見に基づくものです。



**ここでの宗教は、**

**入信前に読んでおいて欲しいものいくつか**

※ お布施をお願いしたり、珍味を売りにうかがうようなことはありませんのでご安心ください。

■ データを取得する

入力されたデータに従って処理が変わってくる、これがサーバサイド処理(CGI,PHPなど)の醍醐味ですからまずはデータ取得から考えていきましょ

CGIの場合 ...取得したデータをまず文字に直す(URLデコード処理)
PHPの場合 ...取得したデータは既に文字に直されている。

ということで、便利な配列$_REQUESTから取得するだけで済みます。もう七面鳥は焼き上がっていてあとは食べるだけといった感じでしょうか

$data = $_REQUEST['key1']; // key1のデータを変数$dataへ代入
if ($_REQUEST['key2'] == '壷') // key2のデータが"壷"かどうか

といった感じで使っていくことになります。


しかーし!


設定にもよりますがPHPでは存在しない(未定義の)データを参照しようとすると警告が出るようになっています。
そこでこの警告を回避するためにも事前に未定義かどうかのチェックをする必要があります。

間違っても設定で警告を出さないようになどと思ってはいけません。それは邪教です。

そこで下記の関数を用意することで、もし未定義ならば自動的に空文字を返すようにすればいいわけです。

isset()という関数(正しくは言語構造)は非常に便利で、未定義の変数でも警告を出さずにチェックしてくれます。
これと似たものにempty()というこれまた便利なものもあります。

と、ここまでは、取得しようとしているデータが未定義(送信されてきていない)なのかも知れない。
食べようと思った七面鳥が実はCG映像なのかも知れない。

ということのチェックでした。


しかし待ってください。どこの誰が送ってきたかわからないデータですよ、まだチェックすべき項目はあります。
では文字コードをチェックしましょう。

一般に、画面表示に使用した文字コードと同一の文字コードでブラウザからは送られてくるものですが絶対というわけではありません。

そこで、どんな文字コードで送られてきてもサーバ側スクリプトで使う文字コード(ここではシフトJIS)に変換しちゃえばいいわけです。



mb_convert_variables()関数は、配列の中身でも一気に変換してくれます。注目すべきはごく短いデータだと文字コードの誤判別が起こりやすいので、内部で全データをつなげてトータルで文字コードを判別してくれる点です。

元の文字コードが判別できなかったり、日本語に変換できないようなデータ(外国の文字やバイナリなど)ならば公式マニュアルではfalseを返すと書いてありますが、わたしの環境だと文字列"pass"を返します。

『SJIS-win』は、現状ではより多くの環境依存文字に対応したシフトJISの指定子です。
『UTF-8』は、言うまでもありません、最近流行りですね。
『CP51932』は、現状ではより多くの環境依存文字に対応した日本語EUCの指定子です。
『JIS』は、わずかな可能性でも拾うぞという意気込みの表れです。

シフトJIS画面表示なので、可能性として一番高いSJIS-winを先頭に書いています。
次に流行りもののUTF-8を書いておきます。
シフトJISと日本語EUCの間では誤判別の問題がありますが、シフトJISのハンカクのみ入力が、日本語EUCの小難しい漢字のみよりも入力される確率が高いのでシフトJISが先というのを考慮しています。

これで、どこの誰が焼いたかわからない七面鳥の丸焼きは自分好みの味にすることができました。

上記は見落としがありました。
どうやらmb系を使って文字コード判別させる際には、"ASCII,JIS,UTF-8"から判別させるように書かないと誤判別の可能性が高くなるようです。
なので書き直すとしたら


でも好みの味付けでも、毒を盛られている可能性があります。
期待していたデータ以外が送られてきたら、これはある種の攻撃(なんとかインジェクション系)かも知れません

これに関しては、次項のバリデーションにて考えるとしましょう。



**福音書その1**
フォームがGET送信指定のデータは$_GETから取得し、POST送信指定のデータは$_POSTから取得すべきという宗派があります。
つまりGETとPOSTどちらでも取得できる便利な$_REQUESTを全否定ということです。
確かに気持ちはわかりますが、ウェブ送信ではこちらの期待に関係なくGETでもPOSTでもCOOKIEでもどうにでも送れてしまいます。
だとしたら、GETで送信されてきたかPOSTで送信されてきたかを分けて書くよりも、結局やるであろうバリデーション(次項でやります)をしっかりやった方がスマートなやり方だとわたしの宗教では洗脳することとします。

■ バリデーションを考える

入力された値をサーバ側で取得したら、その値が妥当なのかどうかすべてチェックする必要があります。
入信を希望する人物が異教徒かも知れないのでその素性を調べ上げることは非常に大事なことです。

これをうまくやれば、なんちゃらインジェクション系の攻撃を未然に防げることでしょう。



まずは必須入力のチェック。先ほど書いたreq()関数を使います。

req()関数を使って取得して、空文字列かどうかチェックするだけで済みます。
比較演算子"==="は、型の一致(この場合文字列を示すsrting型であること)も同一でないとハネてしまうので処理が速いです。
一方、比較演算子"=="は、型が違っても値が同じなら真を返します。(文字列として比較したら違ったから次は数値として比較しようとする分の変換作業に時間がかかるのは当然ですね)



では今度は数値の場合を考えていきます。期待する入力値はいろんなパターンが考えられます。
  • num値は空でもいいのかどうか(必須入力かどうか)
  • 期待した半角数字が入力されたかどうか
  • 数字は0から始まるもの(001 002など)はいいのか
  • 数字の範囲はどうなのか(最大桁数)


以上に基づいて考えていくと

このように、バリデーションではpreg_match()関数使いまくりになります。
しかもD修飾子付きが目立ちますがこれは$が末尾改行にマッチしないようにするためです。



先に書いたreq()関数を使って値を取得する場合、数値を期待するなら

このようにトリミングをしてからチェックしてあげた方がより親切かも知れません。
(入力時にうっかり空白入れてしまう人対策)

あとmaxlength属性値を設定していたところで簡単に乗り越えられてくるという前提で考えれば、あくまでもmaxlengthは飾りとして書いたぐらいのつもりで結局はバリデーションをきっちりやるということを常に意識しておけばよござんす。



余興として日本語(シフトJIS)のいろいろなマッチでも書いておきます。

シフトJISおよびアスキー文字にマッチ

▼ シフトJISの文字コード解剖
範囲正規表現備考
[ぁ-ん]\x82[\x9F-\xF1]「かな」文字
[、]\x81\x41読点
[。]\x81\x42句点
[ー\]\x81\x5B2バイト目が正規表現メタ文字の"["とかち合うので直接文字として書く場合は要エスケープ
[ァ-ヶ]\x83[\x40-\x96]「カナ」文字
[0-9]\x82[\x4F-\x58]全角数字
[A-Z]\x82[\x60-\x79]全角英大文字
[a-z]\x82[\x81-\x9A]全角英小文字
[。-゚][\xA1-\xDF]半角カナ


これを踏まえて、いろいろ書いて行きまっせ



よく掲示板とかで日本語文字が入力されてこなかったら弾くみたいな処理がありますが、一番下のなんかまさにそれですね。


■ 処理の流れを考える

ここでは、一つのファイルで処理を完結させる戒めがありますので少し処理の流れを考えていきましょ


「最初の画面表示」⇒「入力後送信」⇒「メインの処理」⇒「結果レスポンス」

という流れをたどることを考えると、

↓もう少し具体化すると


この中の画面表示の部分に注目してみると、最初の画面表示と最後の結果レスポンスはそれぞれ別の画面表示ルーチンに分かれています。
その他、エラー表示などでも画面表示を行うことになるのでこれら画面表示はすべて統合させたいと思います。

画面表示ルーチンを一つに統合することによって、かなりスッキリできたと思います。

msg(main());としないでmain()内部の最後でmsg()を呼んでもいいですね。

あとはサーバ変数$_SERVER['SCRIPT_NAME']を使って自分自身に送信させる記述は定石として覚えておいた方がいいです。

<form action="{$_SERVER['SCRIPT_NAME']}" method="post">

こう書くことで、後でスクリプト名が変わってもそのまま対応できるということになります。


■ レスポンスを考える

ちょっとしたことですが、考えてみてください。

変数が複数あって、それらを出力するだけでいいとします。
例えば、$val1, $val2, $val3 の3つがあったとします。
さてどのように出力したらいいでしょうか。。。



[1] 3回echoをかます

[2] 1回のechoで連結して出力

[3] 1回のechoで連結しないで出力

[4] 一つの変数にまとめて1回のechoで出力

文字出力という処理が意外とコスト(負荷)のかかる処理になります。
単純な数値演算を1とした場合に文字出力は数百〜数千倍のコストとも言われています。

まぁ人間の感覚では違いは判らないので正しく表示できさえすればいいというのも一理ありますが・・


■ めちゃめちゃ便利な関数を考える

PHPにはただでさえめちゃめちゃ便利な関数が始めから用意されているのですが、人間は欲張りということで、ここではもっとMOTTO便利な関数がないものかと考えることにします。

まず思いつくのが

これはもう殿堂入りでしょ

あと先ほど書いたmsg()関数



すべての出力処理を一箇所で担うことでかなりシンプルにすることをこれで実現できます。
msg()関数内部は引数がなかった場合に初期画面にすり替わる仕掛けになっています。
全画面共通のテンプレートをここに書けば統一感のある画面のスクリプトが実現できます。

併せてエラー表示用のラッパー関数error()もこれを利用することで作ることができました。



これでデータ取得用と画面出力用の両方の便利な関数が揃いました。
この他にどういう関数があれば便利かは難しいですが、データを取得するreq()関数と文字コード変換を一つに統合してはどうだろうと思い作ってみました。



最初にreq()関数をコールしたときに一度だけ文字コード変換をするという処理です。

これで文字コードのバリデーションまでが自動化できた感じですかね

あと必要な処理と言えば細かなバリデーションを行うコンパクトで便利な関数があればよさそうですが、かなり膨大になりそうなので無理そうですね。


■ if文を考える

ある条件のときだけ!この処理をさせたい!!
というときに使うif文ですが、書き方はいろいろあります。その場面でできるだけ適切な書き方をチョイスしたいものです。

if ($foo)
if (isset($foo))
if (empty($foo))
if ($foo === '')
if ($foo === 0)
if ($foo == '')
if ($foo == 0)

いろんな書き方がありますが、変数が未定義かも知れない場合はisset()やempty()を使う必要があります。



未定義かも知れない変数の値が真であることを確認する方法

if (isset($foo) && $foo)
     と
if (!empty($foo))

は等価になります(多分)



empty()は配列でもチェックすることができます。
なので

if (!empty($_FILES)) echo "ファイルアップロードがありました。";


isset()とempty()の配列型の評価の違い

unset($array); // 変数を未定義に
if (isset($array)) // これは配列型もなにもない未定義変数なので「偽」
if (empty($array)) // 未定義変数なので「真」

$array = array(); // 空の配列確保
if (isset($array)) // これは配列型がセットされているので空配列でも「真」
if (empty($array)) // 配列が空なので「真」

$array[] = "foo"; // 配列に要素を追加
if (isset($array)) // これは配列型がセットされているので「真」
if (empty($array)) // 配列に要素があるのでが「偽」


配列処理で注意することは、下記のcount()関数やis_array()関数あるいはforeachループを使った配列処理では未定義の場合に警告が出てしまうこと。

unset($array); // 変数を未定義に
if (count($array)) // 警告が出る

if (is_array($array)) // 配列かどうかをチェックする関数でも警告が出る

foreach ($array as $key => $value) { // 同様に配列処理用ループforeachでも警告が出る
}

未定義では配列型かどうかも未定義なので当然ですね。
ということで未定義をチェックするのは必ずisset()かempty()ということで


■ 文字化け対策を考える

大抵の場合はそのまま表示させてもブラウザの文字コード自動判別機能により、文字化けしないように表示してくれます。

でも、もっと文字化けが起こりにくくなるように「今送ったページは文字コード○○で書かれているよ」と正確に教えてやるのは重要です。
下記の2種類がよく使われている方法です。



header()関数を使う方法は、すべての出力より先に記述する必要があるのでそれなりのコツが要ります。



<meta>タグを使う方法は広く使われているものです。

■ 文字化け以外の文字対策を考える

いわゆるサニタイズ(無毒化)などと言われるものです。

HTMLにおける文字表示では、

タグ記述用の 「<」は「&lt;」、「>」は「&gt;」、
タグ属性値を囲む用の「"」は「&quot;」、
エンティティコードやエンティティ名自身を表示させる用の「&」は「&amp;」


のように記述する決まりがあります。
そしてこの変換は、htmlspecialchars()関数を使うことで行うことができます。

出力データの中には、タグとして記述したものとそうでないものがあるので、これらが混合する前に表示データに対してhtmlspecialchars()関数を施す必要があります。


■ もう一つのバリデーション

定型のリストをチェックする方法を書いておきます。

リストからセレクトボックスを作成するのはforeachで回せば簡単にできます。

こうして作成したセレクトボックスを画面表示させて、選択された情報を取得する場合にバリデーションが必要になります。

この場合は値が連続した数値ではないので複雑なチェック方法になるかと思いきや、配列のキーが値を持つかどうかを見るだけで済みます。



empty()またはisset()を使って簡単にできます。

■ 悟り

ここまでくれば最終解脱は近いでしょう。

わたし自身も最終解脱に向けて日々頑張っているところです。

わたしは参考書・解説本のたぐいは一切買わずにネットの目ぼしい解説サイトと公式ページを回って自分なりの解釈を加えて覚えていくタイプなので大事な部分がすっぽり抜けているかも知れません。
でもこれからも頑張ります。
どこのPHP関係サイトも表示が重すぎて・・・ですが、頑張って情報を集めて回ります。

ちょっとバラバラな解説になってしまいましたが、時間があればもっと整理します。


2010(C)Mingw