1. mod_rewriteを使いこなそう

SEOという言葉が普及して久しいですが、SEO対策の1つとしてURLを最適化するという方法があります。たとえば以下の2つのURLを見比べてみてください。

A.http://phppro.jp/news/detail.php?id=10

B.http://phppro.jp/news/detail/10/

検索エンジンは、AのURL表記よりもBのURL表記を好み、ページの評価が高くなります。このようなURLの最適化をPHP言語だけで処理するのは困難です。そのため、Webサーバーの機能によりURLの「書き換え」を行い、BのリクエストをAのリクエストに移すことで対応を行います。これを実現する仕組みの1つがmod_rewriteモジュールです。

mod_rewriteモジュールは、Apacheに標準で添付されているモジュールで、多くのパッケージでデフォルトで組み込まれています。SEO対策以外にも幅広く使えるモジュールですが、今回は簡単のため、先ほどのURL書き換えの解説を行います。

RewriteEngine On
RewriteRule ^\/news\/([0-9]+)\/? /news/detail.php?id=$1 [L]

RewireEngineディレクティブは、mod_rewrite機能の有効・無効を設定します。そして、RewriteRuleディレクティブで書き換えルールを設定します。RewriteRuleは複数個を記述できます。

RewriteRuleディレクティブは、以下の構文をしています。

RewriteRule 検索パターン 置換文字列 [フラグ1,フラグ2・・・]

検索パターンには、POSIX互換の正規表現を記述できます。タグ付き正規表現にも対応しており、(から)で囲まれた部分は、置換文字列にて$N(ただしNは順番)と参照することができます。

フラグには、置き換えルールの動作を指定します。今回指定しているLは、ここで書き換えを終了することを表しています。要するにRewriteRuleが複数個記述されていた場合にも、その先のRewriteRuleが解釈されなくなる、という事です。

また、RewriteRuleの前にRewriteCondディレクティブを記述することもできます。これは、特定の条件に当てはまる場合にのみ、URLの書き換えるためのもの です。

ただしmod_rewriteは正規表現を使用する関係上、どうしても速度の低下が生じてしまいます。より単純なURLの置換であれば、mod_aliasなどのモジュールを使う方がよいでしょう。

また、PHPの場合はPATH_INFOを使って同様のSEO対策を行うことも可能です。これは、mod_rewriteを使わない方法で、URLはCのようになります。

C.http://phppro.jp/news/detail.php/id/10/

詳しくは、GoogleなどでPATH_INFOを検索してみると良いでしょう。

以上、簡単に解説しましたが、mod_rewriteの備える機能はまだまだあります。詳しい機能の説明は、リンク先のマニュアルやチュートリアルを参照してください。

mod_rewrite
マニュアルの日本語訳:http://www.net-newbie.com/trans/mod_rewrite.html Apache URL Rewriting Guideの日本語訳:http://japache.infoscience.co.jp/rewriteguide/

2. TRUE or FALSE ?

入力チェックをする際や関数の引数を比較する際、値の中身だけでなく値の型も比較しないと誤作動を起こしてしまう可能性があります。

空文字や0、nullやFALSEの違いを把握し、適切な式で値判定をしていますか?

入力フォームで、金額を入力する部分を作ったとします。必須項目ではないので、何かしらの数字が入力されていたらDBに登録する処理を作ったとします。

//記入があったら
if (!$_POST["price"]) {
  //以下登録処理

}

以上のように、$_POST["price"]に対して!を用いてTRUEかFALSEの判定をしています。 この例ですと、"0"を入力された場合にもFALSEの判定がされ、登録処理が行われない事になってしまいます。実際のところ「数字が記入されていたら」という判定をするのが一番なのですが、簡単に済まそうと!を不用意に用いると思わぬ穴を作る場合があります。

次の例ではどうでしょうか。

配列$data_listの値の中に、指定された$find_dataがあるかどうか探し、もしあったらそのキーを表示するというスクリプトを作ります。array_search関数を使い、見つからなかったらFALSEを返すのですから、以下のように書きました。

$array = array(0 => 'data1', 1 => 'data2', 2 => 'data3', 3 => 'data4');

$key = array_search($find_data, $array);
if ($key != FALSE) {
  //発見
  print $key;
} else {
  //発見できず
  print "見つかりませんでした";
}

この例ですと、$find_dataで探そうとしていた値が$arrayの[0]に入っていた場合、$keyには0が入ります。ですが$keyの値「0」はFALSEと評価されてしまうので、「見つかりませんでした」と表示されてしまい、見つかった処理にはいけません。

このようなFALSEと評価される答えを返す場合がある時は特にですが、値の型も考慮して比較しましょう。

if ($key !== FALSE) {
  //発見
  print $key;
} else {
  //発見できず
  print "見つかりませんでした";
}

型まで比較しようと思うならば、$a === $b($a !== $b)などのように=を3つ使った比較演算子を使います。

参考:http://jp.php.net/manual/ja/language.operators.comparison.php

PHPのマニュアルには、空文字やNULL、FALSEや0などの型において、それぞれのTRUEやFALSEを記した型の比較表という付録がついています。全てを厳密に覚えている人は少ないかも知れませんが、より正確なスクリプトを組む際はある程度の知識は必須になるでしょう。

これくらい自信があるよ、という人はPHP Syntax Examというサイトがありますので、こちらで腕試しをしてみるのも面白いでしょう。上記の型比較の他にも、色々な形式でTRUEかFALSEかを問われます。あなたは何点取れるでしょうか?

BlueShoes: PHP Syntax Exam:http://www.blueshoes.org/en/developer/syntax_exam/

3. 基礎構文処理速度のあれこれ

なんとなく結果が分かってはいるけれど、基礎的なPHPの構文がどれだけ処理に時間がかかっているかを算出してみました。マシンスペックによって結果は異なってきますが、簡単なスクリプトを用意して速度を測ってみます。

まずは文字列の解析にどれだけ時間がかかっているかをチェックしてみました。ダブルクォーテーション内は変数が展開される為、その分のオーバーヘッドがかかると思います。

<?php
ini_set
('max_execution_time','100');
$loop_counter 1000000;

/**
 * クォーテーションチェック
 */
// PHP5からはmicrotime()に引数を渡すとfloatで返してくれるのです☆
$single_start_time microtime(true);
$single_check array();
for (
$i=1$i<=$loop_counter$i++) {
    
$single_check["abc"] = "PHPプロ!";
}
$single_end_time microtime(true);

$double_check array();
$double_start_time microtime(true);
for (
$i=1$i<=$loop_counter$i++) {
    
$double_check['abc'] = 'PHPプロ!';
}
$double_end_time microtime(true);

$single_loop_time $single_end_time $single_start_time;
$double_loop_time $double_end_time $double_start_time;

print 
$single_loop_time."<br>\n";
print 
$double_loop_time."<br>\n";
?>

多少の差はあるものの、大きなオーバーヘッドは発生しないようです。次にインクリメントの記述方法で速度が変わるかをチェックしてみました。パターンは

  • $i++
  • ++$i
  • $i = $i + 1
  • $i += 1

の4パターンです。

<?php
/**
 * インクリメントチェック
 */
$check1_start_time microtime(true);
for (
$i=1$i<=$loop_counter$i++){}
$check1_end_time microtime(true);

$check2_start_time microtime(true);
for (
$i=1$i<=$loop_counter; ++$i){}
$check2_end_time microtime(true);

$check3_start_time microtime(true);
for (
$i=1$i<=$loop_counter$i $i 1){}
$check3_end_time microtime(true);

$check4_start_time microtime(true);
for (
$i=1$i<=$loop_counter$i += 1){}
$check4_end_time microtime(true);


$check1_loop_time $check1_end_time $check1_start_time;
$check2_loop_time $check2_end_time $check2_start_time;
$check3_loop_time $check3_end_time $check3_start_time;
$check4_loop_time $check4_end_time $check4_start_time;

print 
$check1_loop_time."<br>\n";
print 
$check2_loop_time."<br>\n";
print 
$check3_loop_time."<br>\n";
print 
$check4_loop_time."<br>\n";
?>

どうやら前置加算子が一番早いようです。前置加算子・後置加算子の違いはこちら(http://www.phppro.jp/phpmanual/php/language.operators.increment.html)

最後にループの比較をしてみました。大きく分けて

  • for文
  • while文
  • do-while文
  • foreach文

の速度を比較しています。

一つのfor文をとってもいくつかの記述方法があります。

  • ループ範囲をブレス{}で囲う
  • 1行ループの{}ブレスを省く
  • ループ範囲を for(): endfor; で囲う

の比較をしてみました。また、foreach文とwhile(each())を使ったケースを比較しました。

<?php

/**
 * for文チェック
 */
$for1_start_time microtime(true);
for (
$i=1$i<=$loop_counter; ++$i) {$a=0;}
$for1_end_time microtime(true);

/**
 * for文チェック(記述別)
 */
$for2_start_time microtime(true);
for (
$i=1$i<=$loop_counter; ++$i)
$a=0;
$for2_end_time microtime(true);

/**
 * for文チェック(記述別)
 */
$for3_start_time microtime(true);
for (
$i=1$i<=$loop_counter; ++$i):
$a=0;
endfor;
$for3_end_time microtime(true);

/**
 * while文チェック
 */
$while_counter 1;
$while_start_time microtime(true);
while (
$while_counter <= $loop_counter) {
  
$a=0;
  ++
$while_counter;
}
$while_end_time microtime(true);

/**
 * do-while文チェック
 */
$dowhile_counter 1;
$dowhile_start_time microtime(true);
do {
  
$a=0;
  ++
$dowhile_counter;
} while (
$dowhile_counter $loop_counter);
$dowhile_end_time microtime(true);

// $loop_counter個の配列を作っておきます
$test_array array();
for (
$i=1$i<=$loop_counter$i++) {
  
$test_array[$i] = $i;
}

/**
 * while(each())文チェック
 */
$list_while_start_time microtime(true);
while (list(
$key$value) = each($test_array)) {$a=0;}
$list_while_end_time microtime(true);

/**
 * foreach文チェック
 */
$foreach1_start_time microtime(true);
foreach (
$test_array as $key => $value) {$a=0;}
$foreach1_end_time microtime(true);

/**
 * foreach文キーなしチェック
 */
$foreach2_start_time microtime(true);
foreach (
$test_array as $value) {$a=0;}
$foreach2_end_time microtime(true);


$for1_loop_time        $for1_end_time       $for1_start_time;
$for2_loop_time        $for2_end_time       $for2_start_time;
$for3_loop_time        $for3_end_time       $for3_start_time;
$while_loop_time       $while_end_time      $while_start_time;
$dowhile_loop_time     $dowhile_end_time    $dowhile_start_time;
$list_while_loop_time  $list_while_end_time $list_while_start_time;
$foreach1_loop_time    $foreach1_end_time    $foreach1_start_time;
$foreach2_loop_time    $foreach2_end_time    $foreach2_start_time;

// 通常for文を1とした時の速度率を出力しています。
$for1_rate       $for1_loop_time       $for1_loop_time;
$for2_rate       $for2_loop_time       $for1_loop_time;
$for3_rate       $for3_loop_time       $for1_loop_time;
$while_rate      $while_loop_time      $for1_loop_time;
$dowhile_rate    $dowhile_loop_time    $for1_loop_time;
$list_while_rate $list_while_loop_time $for1_loop_time;
$foreach1_rate   $foreach1_loop_time   $for1_loop_time;
$foreach2_rate   $foreach2_loop_time   $for1_loop_time;

print 
$for1_rate."<br>\n";
print 
$for2_rate."<br>\n";
print 
$for3_rate."<br>\n";
print 
$while_rate."<br>\n";
print 
$dowhile_rate."<br>\n";
print 
$list_while_rate."<br>\n";
print 
$foreach1_rate."<br>\n";
print 
$foreach2_rate."<br>\n";

?>

for文は記述方法で処理速度に大きな違いはほとんど見られないようです。違いがあったら面白かっただけに、残念です。

次にwhileとdo-whileの比較です。若干do-whileの方が早いようですがこれ位の差であれば、可読性を重視するなら見慣れた通常のwhile文を使うのがよさそう ですね。

最後にforeachとwhile(each())の比較です。私は普段からforeachを使っており、遅いとの噂があったのでwhileに期待をしていたのですが、大差をつけてのforeach勝ちでした。また、foreachの要素キーを取得する$keyを省くとかなり1.1倍~1.3倍程高速という結果になりました。ループ内で要素キーを使わない場合は省いてしまったほうが良さそうです。

実行するマシンスペックや、ループコード内に不平等はありますが、処理速度を総合的に見るとwhile文が一番早いようです。

上記結果を踏まえて、コード規約を一度見直しては如何でしょうか?

バックナンバーについて

TIPS-MLは、毎週金曜日に更新され、新しい記事が掲載されます。