[PHP]PHPプログラミング学習者が陥りやすい10の失敗

「入門書のとおりに作っていれば失敗はないはず」という考えはそもそも失敗です。
このブログもそうですが、多くのネットや参考書のサンプルは要点を短く書くために重要な下準備を省略することが多く、わかりきったこととして深く取り扱っていない場合がほとんどです。
そうした予備知識を持たずにプログラムを書き始める際に起こりうる 10 の失敗と対処法について考えてみます。


1. htmlspecialchars() をし忘れる

HTML として変数の内容を出力する際、「<」「>」などの特殊文字をエスケープ(無害化)するために htmlspecialchars() を通す必要があります。
任意のスクリプトを実行されてしまう脆弱性の原因にもなるので、変数に入るものがわかりきっていても htmlspecialchars() を通すくらいでいいと思います。
多くの人は省略して h() という関数を定義します。ENT_QUOTES でシングルクォートもエスケープするようにします。

function h($str, $encode='UTF-8'){
    return htmlspecialchars($str, ENT_QUOTES, $encode);
}

2. 比較に「==」を使ってしまう

十分に理解して使う場合を除き、「==」による比較はおすすめできません。
if 等で 「==」を使うと型を区別しない比較が行われるため意図しない判定結果が出る可能性があります。
null, false, 0, 空文字 などをしっかり区別し、「===」による比較を使うのが無難です。
また、switch() での条件比較にも内部的に「==」が使われているため、注意が必要です。


3. $_GET, $_POST などのパラメータを直接扱う

基本的にユーザーが送ってくるデータは危険なものであるという前提でプログラムを組む必要があります。
文字列を想定しているのに配列が送られてきたり、数値が入るべきところに文字列が入力される可能性もあります。
悪意あるユーザーは用意された入力画面以外からデータを送ってきたり、NULL バイトを紛れ込ませてチェックをすり抜けようとしてきます。
PHP には filter_input() という関数が用意されており、簡単に内容をチェックしたりフィルターしたりすることができるので、そちらを利用してどんな値が来ても対応できるようにすべきです。


4. mysql 系などの非推奨関数を使ってしまう

PHP に限らずプログラミングの世界は変化が激しく、少し前まで常識だった手法がいきなり非推奨となる事が多いです。
古い参考書には mysql_connect() や mysql_real_escape_string() などの関数を使ってデータベースとやりとりするサンプルがありますが、現在ではより安全な mysqli 系の関数や PDO を利用することが必須となっています。
定期的にマニュアルを見る習慣を作り、最新の情報を知って置かなければいけません。


5. header() より前に文字列が紛れ込む

header() や session_start() を使用するより前にいかなる文字も出力してはいけません。
改行や空白文字が混入してしまうと「headers already sent」というエラーが表示されることになります。
include しているファイルの末尾が「?>(改行)」のようになっていたり文字コードに BOM 付きの UTF-8 を使っていた場合によく発生します。省略可能な「?>」は省略し、BOM なしの UTF-8 を利用して対処します。


6. 実行の途中で環境が変わる可能性を考慮していない

<?php
$month = date('m');
$day   = date('d');
$hour  = date('H');
$min   = date('i');

日時を変数の格納する際上のようにしてしまうケースは時々見かけます。
プログラムは一瞬で実行されて一見なんの問題もないように思ってしまいますが、深夜零時に差し掛かったころにこのプログラムを実行し、運悪く $hour を取得するタイミングで日付が変わってしまったら月日は前日のもの、時刻は当日のものという結果になってしまいます。
DateTime クラスを使って取得を一度きりにし、誤った日時取得の可能性をなくします。

$now = new DateTime();
$month = $now->format('m');
$day   = $now->format('d');
$hour  = $now->format('H');
$min   = $now->format('i');

7. マルチバイト非対応の関数を使ってしまう

PHP ではマルチバイト関係の関数の頭には mb_ がついています。
文字数を取得する関数 strlen() とマルチバイトを考慮した文字数を取得する mb_strlen() では結果が大きく異なるのですぐに気がつくと思いますが、str_replace() など、文字コードなどの条件によって問題が起こるケースでは気づかずに使ってしまっていることが有ります。海外のソースコードではマルチバイトを考慮していない場合が多く、そのまま使えないことも有ります。
文字列を操作する関数を使うとき、マルチバイト対応版がないか、正規表現では「/~/u」などのオプションがないかマニュアルでチェックしましょう。


8. クッキーや偽装可能なリファラーなどを過信してしまう

クッキーは一時的にデータを保存する際に使われますが、その内容はクライアント側(サイトの訪問者)が自由に書き換えできます。
また、ブラウザによって格納可能な文字数に制限がある場合もあり、注意が必要です。
クッキーに値を保存する場合、内容を改ざんされても問題のない仕組みになっているか、重要な情報を保存していないかを十分に確認する必要があります。
リファラーや IP、ユーザーエージェントなども確実に正しい情報が得られるとは限りません。全く参考にならないわけではないですが注意して扱う必要があります。


9. 「__DIR__」を使わず相対パスで include

「__DIR__」あるいは「dirname(__FILE__)」にはそのプログラムファイルのある場所の絶対パスが格納されています。
include で相対パスを指定した場合、実行時のフォルダ階層によって相対パスの指し示すファイルの階層が変わってしまうため、「__DIR__」を使って絶対パスで指定するのがオススメです。


10. 自作メールフォームを悪用されてしまう

メールフォームを自作するのに必要なセキュリティ上の知識はかなり多く、踏み台として悪用されてしまうとそのサイト以外にも被害が拡大します。
正しい入力フォームからデータを受け取っているか、受け取った内容に危険な文字が含まれていないか、宛先を書き換えられたり、送信控えとして任意の相手に自由なメッセージを送信できる可能性はないか、BOT を使った自動スパムや過度な連続投稿を防ぐことができるかなど、気をつけるべき項目が多く、PHP を使ったプログラムの中でも最も脆弱性が潜んでいる可能性が高いので、十分な知識なしにサンプルソースを寄せ集めてメールフォームを作るのはかなり危険です。


以上 10 点を例として挙げました。
PHP 自体も発展途上な部分が多くあり、不定期に変更が行われています。常に最新の安全な状態を維持しなければいけないという意味では、PHP の熟練者は存在しないと言えます。既知のことや習熟した内容であってもニュースやマニュアルをチェックするように心がけたいものです。

[PHP]Fatal Error発生時にログを作成する

Fatal Error 発生時、以降の処理は中断されるため通常は try ~ catch などでエラーを捕捉することができません。
そこで、register_shutdown_function() を使ってスクリプト終了時に関数を実行し、エラーの内容をテキストとして保存します。

<?php
register_shutdown_function('shutdown');

$test = new Test();

function shutdown(){
	$error = error_get_last();
	
	if(empty($error)) return;
	
	switch($error['type']){
		case(E_ERROR):   $type = "Fatal Error"; break;
		case(E_WARNING): $type = "Warning"; break;
		case(E_NOTICE):  $type = "Notice"; break;
		default:         $type = "Error";
	}
	
	$date = new DateTime();
	$line = sprintf("%s: %s. %s in %s on line %s\n",
		$date->format('Y-m-d H:i:s'), $type, $error['message'],
		$error['file'], $error['line']);
    file_put_contents(dirname(__FILE__) . "/log.txt", $line,
		FILE_APPEND | LOCK_EX);
}

【出力結果】

2015-02-23 08:03:36: Fatal Error. Class 'Test' not found in C:\xampp\htdocs\lab\index.php on line 4

存在しないクラスをインスタンス化しようとして Fatal Error が発生しています。
error_get_last() で最後に発生したエラーを取得できるので、それを整形してログファイルに追記します。

参考: http://php.net/manual/ja/function.register-shutdown-function.php

[PHP]3次スプライン曲線を使ったスプライン補間

点と点を線で結んでグラフにする時、点同士の間を補完する必要があります。
単純に直線でつなぐだけの線形補間でもグラフにはできますが、全ての点を通る滑らかな曲線を描画するのであれば3次スプライン曲線を利用するのが一般的です。

spline2
spline1

<?php
$points = array();

$width  = 300;
$height = 100;

for($x=0; $x<=$width; $x+=50){
	$y = rand(0,100);
	$points[] = array($x, $y);
}
$spline = new CubicSpline($points);

$spline->draw($width, $height);

class CubicSpline
{
	private $xx = array();
	private $yy = array();
	private $q = array();
	private $r = array();
	private $s = array();
	private $n;
	private $points;
	
	
	function __construct($points){
		$this->points = $points;
		
		$this->n = $n = count($points) - 1;	// 区間の数
		$h = array();
		$b = array();
		
		// Step1
		for($i=0;$i < $n;$i++){
			$h[$i] = $points[$i + 1][0] - $points[$i][0];
		}
		
		for($i=1;$i < $n;$i++){
			$b[$i] = 2.0 * ($h[$i] + $h[$i - 1]);
			$d[$i] = 3.0 * (($points[$i + 1][1] - $points[$i][1])
				/ $h[$i] - ($points[$i][1] - $points[$i - 1][1]) / $h[$i - 1]);
		}
		
		// Step2
		$g[1] = $h[1] / $b[1];
		for($i=2;$i<$n - 1;$i++){
			$g[$i] = $h[$i] / ($b[$i] - $h[$i - 1] * $g[$i - 1]);
		}
		
		$u[1] = $d[1] / $b[1];
		
		for($i=2;$i<$n;$i++){
			$u[$i] = ($d[$i] - $h[$i - 1] * $u[$i - 1]) / ($b[$i] - $h[$i - 1] * $g[$i - 1]);
		}
		
		// Step3
		$this->r[0] = $this->r[$n] = 0.0;
		$this->r[$n - 1] = $u[$n - 1];
		for($i=$n - 2; $i >= 1; $i--){
			$this->r[$i] = $u[$i] - $g[$i] * $this->r[$i + 1];
		}
		
		//Step4
		for($i=0;$i<$n;$i++){
			$this->q[$i] = ($points[$i+1][1] - $points[$i][1]) / $h[$i] - $h[$i]
				* ($this->r[$i+1] + 2.0 * $this->r[$i]) / 3.0;
			$this->s[$i] = ($this->r[$i + 1] - $this->r[$i]) / (3.0 * $h[$i]);
		}
		
		return;
	}
	
	function value($x){
		$points = $this->points;
		$n = $this->n;
		$i = -1;
		
		// 区間の決定
		for($i1=1;$i1 < $n && $i < 0;$i1++){
			if($x < $points[$i1][0]){
				$i = $i1 - 1;
			}
		}
		if($i < 0){
			$i = $n - 1;
		}
		
		// 計算
		$x2 = $x - $points[$i][0];
		$y = $points[$i][1] + $x2
			* ($this->q[$i] + $x2 * ($this->r[$i] + $this->s[$i] * $x2));
		return $y;
	}
	
	public function draw($width, $height, $hMargin=10, $vMargin=30 ){
		$end = end($this->points);
		
		$image = imagecreatetruecolor($width + $hMargin * 2,
			$height + $vMargin * 2);
			
		$bg     = imagecolorallocate($image, 255, 255, 255);
		$axis   = imagecolorallocate($image, 180, 180,180);
		$color1 = imagecolorallocate($image, 30, 30, 30);
		$color2 = imagecolorallocate($image, 50, 150, 255);
		imagefill($image, 0, 0, $bg);

		imageline($image, $hMargin, $height + $vMargin,
			$hMargin + $width, $height + $vMargin, $axis);
		imageline($image, $hMargin, $vMargin,
			$hMargin, $height + $vMargin, $axis);
		
		$x1 = $y1 = $x2 = $y2 = 0;
		
		for($x=0;$x<$end[0];$x++){
			$x1 = $x2;
			$y1 = $y2;
		
			$x2 = $hMargin + $x;
			$y2 = ($height + $vMargin) - $this->value($x);
			if($x == 0) continue;
		   imageline($image, $x1, $y1, $x2, $y2, $color1);
		}
		
		foreach($this->points as $point){
			list($x, $y) = $point;
			$x = $hMargin + $x;
			$y = ($height + $vMargin) - $y;
			imagefilledrectangle($image, $x - 2, $y - 2,
				$x + 2, $y + 2, $color2);
		}
		
		header('Content-Type: image/jpeg');
		imagejpeg($image, null, 90);
		imagedestroy($image);
		 
		exit;
	}
}

サンプルではランダムに点をプロットしてそれらの点を通るスプライン曲線を描画しています。
クラスをインスタンス化する際に座標 array(x, y) を配列にしたものを渡し、draw() で画像として描画します。

このプログラムは下記参考 URL のソースコードを元に PHP で書きなおしたものです。

参考:
http://www.sist.ac.jp/~suganuma/cpp/2-bu/7-sho/7-sho.htm#e-7-42