2011年8月29日月曜日

phpの出力制御

header()の前には出力はできない(はず)

phpのマニュアルのheader()関数の説明に
<html>
<?php
/* これはエラーとなります。この上に出力があることに注目してください。
* それはheader()のコールより前であるということになります */
header('Location: http://www.example.com/');
?>
とあります。たしかにおぼろげに
Cannot modify header information - headers already sent by ...
こんなワーニングを見た記憶があります。
このワーニングを再現しようと思い上記のコードをテストサーバに配置しアクセスしてみたのですが、ワーニングも出ずにきちんとリダイレクトされてしまいました。なぜ!? 納得がいくまで調べていたら数日が経過してしまいました…

調査

サーバからのレスポンスをヘッダを含めて確認したかったので、いろいろ手段はありますが今回は Burp Suite を使いました。
レスポンスはこのようになっていました。
HTTP/1.1 302 Found
Date: Mon, 29 Aug 2011 **:**:** GMT
Server: Apache/2.2.16 (Ubuntu)
X-Powered-By: PHP/5.3.3-1ubuntu9.5
Location: http://www.example.com/
Vary: Accept-Encoding
Content-Length: 7
Content-Type: text/html

<html>
<html>を先に出力しているのに、きちんとheader()関数が有効で、Location:ヘッダが出力されていました。これは明らかに、

出力バッファリング

ですよね。でも、出力制御関数は一切使っていないのに…となると、答えはもう、php.iniしかありません。
テストした環境(ubuntu server 10.10, php 5.3.3)のphp.iniを見てみると…
output_buffering = 4096
見つけました。この設定があるおかげで、header()の前に出力があってもheader()が有効でした。ためしに、
<html>
<?php
echo str_repeat(" ", 4096);
header('Location: http://www.example.com/');
?>
このように余計な文字列を出力し Content-Length: が4096以上になると、その時点でバッファに溜め込んだ出力がヘッダとともに出力されてしまうので、次の header("Location:"... でワーニングが発生し、リダイレクトもされません。

以上をふまえて注意

header()を使うときは
  • header()とコンテンツの出力の順序に気をつける。これはMVCの分離ができていれば容易にできるはずです。
もしくは
  • output_bufferingの設定を明示的に行う。php.ini以外に.htaccessやhttpd.confでも可能です。
header()がまともに動かなかったり動いたり、出力制御が結果的にややこしくしている原因かもしれません。しかし、phpの出力制御の特性を理解していればトラブルもすぐ解決できることでしょう。

2011年8月23日火曜日

phpでテキストファイルを読み込む方法あれこれ

どんな言語でプログラミングしていても、テキストファイルを読み込んで処理するというのは基本中の基本です。phpではいくつかの方法が用意されています。

まずは、読み込むファイルを用意しました。「なんちゃって個人情報」さんを使わせていただきました。

三上 恭子
藤岡 六郎
吉村 隼士
沢尻 由宇
朝倉 光博
今回取り上げるのは
  • file
  • file_get_contents
  • fopen - fgets
の3つです。
<?php
// file() 1行1要素の配列に読み込む
$list = file("sample.txt");
var_dump($list);

// 全体をひとつの文字列に読み込む
$s = file_get_contents("sample.txt");
var_dump($s);

// 1行ずつ読み込む
$fp = @fopen("sample.txt", "r");
while (($line = fgets($fp))) {
	$line = rtrim($line);
	echo $line . PHP_EOL;
}
@fclose($fp);
巨大なファイルを file や file_get_contents で読み込もうとするとメモリ不足のエラーが発生する可能性がありますので、ケースによって1行ずつ読み込む昔ながらのfopen - fgets と使い分けるとよいでしょう。

さきほどのスクリプトの実行結果は以下になります。

array(5) {
  [0]=>
  string(14) "三上 恭子
"
  [1]=>
  string(14) "藤岡 六郎
"
  [2]=>
  string(14) "吉村 隼士
"
  [3]=>
  string(14) "沢尻 由宇
"
  [4]=>
  string(14) "朝倉 光博
"
}
string(70) "三上 恭子
藤岡 六郎
吉村 隼士
沢尻 由宇
朝倉 光博
"
三上 恭子
藤岡 六郎
吉村 隼士
沢尻 由宇
朝倉 光博

SyntaxHighlighter 再び

先日の記事「シンタックスハイライティング」で当ブログにSyntaxHighlighterを組み込んだのですが、そのときから感じていたのが、「javascriptを大量に読み込んでるなぁ」でした。よく使うファイル形式に絞って読み込むという方法も考えられますが、公式サイトで説明されている"autoloader"をどうせなら使ってみようと思い立ったものの、またもやハマってしまいました。

テンプレートが保存できない

前回同様、前に公式ページを参考にjavascriptを配置して、「テンプレートを保存」したところ、エラーが発生しました。
テンプレートの形式が適切でないため、解析できませんでした。 すべての XML 要素が適切に閉じられているかどうかを確認してください。
XML エラー メッセージ: The content of elements must consist of well-formed character data or markup.
これは、しばらく考え込んだ結果、javascript中の不等号'<'を'&lt;'に書き換えればよいことに気がつきました。アポストロフィは保存時に勝手に書き換えるのに、不等号はやってくれないのですね…

動いているようで動かない

これでテンプレートの保存ができたので、期待を込めて「プレビュー」したのですが、シンタックスハイライティングが有効になりません。javascriptの文法に問題があるかもと、Chromeのデベロッパーツールで確かめてみたところ、スクリプト自体の動作は問題ないようです。

途方にくれ、web上をいろいろ検索していたところ、window.onloadのイベントで初期化を行えばよいということが判明しました。公式サイトのExamplesを見てもそういう説明はないのですが、autoloaderを使うときは初期化タイミングに気をつける必要があるようです。(ソースコードを追う気になれず:-;)

ようやく動いたのが下のコードです。





これで読み込むjavascriptの数も減って、多少軽はくなったんでしょうか。ブラウザのキャッシュに一回入ってしまえば差がないのでは?という気がしないでもないです。

2011年8月21日日曜日

phpで必須パラメータの指定忘れをチェック

phpでパラメータが多い関数を作るとき、このように引数をずらずら並べるのはいい方法ではありません。
function fooBar($name, $age, $mail, $sex, $state)
{
...
}
関数仕様が変更されたとき、呼び出し側のインパクトが大きいからです。phpはスクリプト言語のため、事前にコンパイルして引数の数が一致しているか検知することもできません。

この解決策として、パラメータをオブジェクトや連想配列で渡すように設計するのですが、今回は連想配列の一例です。

/**
 * パラメータチェックサンプル
 *
 * @param array 'name', 'age', 'sex', 'state'をキーにもつ連想配列
 */
function fooBar($params)
{
	$diff = checkEssentialParams($params, array('name', 'age', 'sex', 'state'));
	if (count($diff) > 0) {
		// $diffには不足しているパラメータ
		echo print_r($diff, true) . "パラメータが指定されていません。\n";
		return false;
	}
}
function checkEssentialParams($params, $essential)
{
	return array_diff($essential, array_keys($params));
}


fooBar(array("age" => 30));

checkEssentialParams関数の中で array_diff を使って連想配列中のキーのセット忘れをチェックしています。

実行結果

> php check_essential_params.php
Array
(
    [0] => name
    [2] => sex
    [3] => state
)
パラメータが指定されていません

2011年8月20日土曜日

phpのキャスト

Zend Frame Workのバリデータの挙動がなかなか思い通りにいかず、仕方なくコードをトレース実行していたところ、見慣れないキャストを見つけました。
foreach (array_merge(array_keys($this->_missingFields), array_keys($this->_invalidMessages)) as $rule) {
	foreach ((array) $this->_validatorRules[$rule][self::FIELDS] as $field) {
		unset($this->_data[$field]);
	}
}
(array) $this->_validatorRules[$rule][self::FIELDS]
arrayへのキャストですか? C,C++ではキャストといえばスカラー型どうしか、あってもポインタのキャストしか触ったことがないので違和感があります。ということで、調べてみました。

phpのマニュアルの「型の相互変換」のページには、キャストの一覧にこう書いてあります。

使用可能なキャストを以下に示します。
  • 整数へのキャスト
  • (bool), (boolean) - 論理値へのキャスト
  • (float), (double), (real) - float へのキャスト
  • (string) - 文字列へのキャスト
  • (array) - 配列へのキャスト
  • (object) - オブジェクトへのキャスト
  • (unset) - NULL へのキャスト (PHP 5)
(array), (object), (unset)あたりがクセモノですねぇ。(unset)なんて使い道すら想像できません。
arrayへのキャストは、マニュアルを読めばすぐ理解できました。
要素1つの配列に変換するというキャストでした。

これが何の使い道があるかというと、phpの関数の引数のアバウトさと関係しています。phpは関数の引数のチェックを厳密に行う必要がないため、スカラ型でも配列でも引数としてokという関数が作成できます。
そして、関数内部では(array)にキャストしてやれば、スカラ型が渡されたとしても配列として扱うことができます。
簡単な例です。

function echoList($list)
{
	$list = (array)$list;
	foreach ($list as $element) {
		echo $element . PHP_EOL;
	}
}
キャストすることで、if (is_array($list)) ... と分岐する必要がなくなります。

2011年8月18日木曜日

javascriptで部分文字列を扱う

javascriptで文字列の一部を取り出したいとき、substrを使うことが真っ先に思い浮かんだのですが(phpで慣れ親しんでいますし)実際に使用してみて思いもよらぬ不具合に遭遇してしまいました。

以下のコードは、文字列 s の一番最後の文字の一つ手前の文字を取りだすつもりで書いています。

funtcion substr_test()
{
	var s = "0123456789";
	alert(s.substr(-2, 1));
}

手元の"Google Chrome 13.0.782.112 m"や"FireFox 6.0"では期待通りの "8" が表示されるのですが、IEで期待通りの値を返してきません。
リファレンスをよく読むと、
注: start の引数に負の数の値を指定することは、Microsoft JScript [1] ではサポートされません【訳注: JScript で start に負の数を指定した場合、正の数として扱われます】
とありました。

IE8, IE9で動作を確認してみたところ、「互換表示」が有効かどうかで結果が変わることがわかりました。

IE8(互換表示有効)IE8(互換表示無効) IE9(互換表示有効)IE9(互換表示無効)
動作 × ×

対策

startに負の数が指定された場合は、文字列長から逆算して正の数を指定してやれば解決です。ついでに、取り出す文字列長にも負の数を指定できるようにして、phpと同じ挙動にした substr_ex メソッドを作ってみました。
String.prototype.substr_ex = function(start, length) {
	var l = this.length;
	if (start < 0) {
		start = l + start;
	}
	if (length < 0) {
		length = l - start + length;
	}
	return this.substr(start, length);
}

2011年8月16日火曜日

シンタックスハイライティング

ブログはじめました」のエントリを書いて、次はまともな記事を書こう!と意気揚々としていたところ、早速ハマってしまいました。

タイトルのとおり、シンタックスハイライティングです。
ブログの性質上プログラムを載せることは多くなるだろうし、巷のブログでよく見かける装飾されたプログラム表示をしてみたいなぁ、というのは自然の流れです(よね?)

さっそく調べてみると、有名なシンタックスハイライティングのスクリプトとして"SyntaxHighlighter"が見つかりました。
Bloggerへの導入も"integration"のページから参考になるリンクが多数あるので、すんなり導入できました。「デザイン」タブ→「HTMLの編集」→</head>前に、ここで紹介されているコードを置くだけで完了。

function substr_test()
{
	var s = "0123456789";
	alert(s.substr(-3));
}

ちゃんとシンタックスハイライティングされてますかね?
これでプログラムつきの記事が書けるようになりました!
そして、本題の記事はまた次回へと持ち越しになるのでした…

ブログはじめました

いままで何度ブログを書き、放置してきたことか…
思い返すと、もともと多趣味な自分がテーマもなしに漠然と書きたいことをつらつら並べていただけで、「読んでもらう」ということはまったく念頭になかったのが結局長続きしなかった原因だったのでは?と思っています。

そこで、今回は技術ネタに絞ってブログを書いていこうと思います。ネタは常に乏しいかもしれませんが、「プログラミング」をしていて日々気づいたTipsなどを、自分の備忘録として、また、検索エンジンにでも引っかかって訪問されたかたのお役に立てれば幸いです。

当ブログの名前の由来ですが、私が愛読している"CodeUtopia"さんから「インスパイア」させていただきました。