先日公開したモダンPHPアンチパターンの記事で、「namespaceを利用しない」の項目に「BOMが出力された怖い話」を書いたところ、いまいち理解されてない感があるので、地面に吐いた唾を飲み込む心意気で自分のギャグをことこまかに紹介したい。
過去に承認欲求欲しさに自分の靴を舐めたシリーズ
さておき、実際の例を見せる
説明の前に、本稿の内容が有効であることをコード例をもって示す。使用したバージョンはOS X標準添付のPHP 5.5.27(cli)だ。
以下のコードはわかりやすさのため、Z Shellで実行する。エスケープめんどくさいので…
% echo 'hello' | php
hello
% echo 'hello\n<?php' | php
hello
% echo '<?php echo "hello", PHP_EOL;' | php
hello
% echo '<?php namespace dummy; echo "hello", PHP_EOL;' | php
hello
% echo 'hello\n<?php namespace dummy;' | php
Fatal error: Namespace declaration statement has to be the very first statement in the script in - on line 2
% echo '<?php namespace dummy; echo "hello", PHP_EOL;' | nkf --oc=UTF-8-BOM | php
Fatal error: Namespace declaration statement has to be the very first statement in the script in - on line 1
この例ではUTF-8にBOMを付加するためにnkfコマンドを利用した。
BOMとは何か
バイトオーダーマーク (byte order mark) あるいはバイト順マーク(バイトじゅんマーク)は通称BOM(ボム)といわれる、Unicodeの符号化形式で符号化したテキストの先頭につける数バイトのデータのことである。このデータを元にUnicodeで符号化されていることおよび符号化の種類の判別に使用する。
(バイトオーダーマーク - Wikipedia 2013-03-20T15:26:14版より引用)
要はUnicodeで符号化されたテキストファイルの先頭にあるバイトで、これを読み取ることによってエンコーディングの種類(UTF-16, UTF-32, UTF-8)とバイトオーダー(ビッグエンディアン, リトルエンディアン)を判別できる。これはUTF-8にも規定されてるのだけれども、実際のところUTF-8には不必要なものであって基本的には付けるべきではない。BOMなしのUTF-8をUTF-8Nと呼ぶ謎の慣習(?)がある(謎)。
PHPとテンプレートエンジン
PHPはチューリング完全性とオブジェクト指向機能を持った、世界でもっとも著名なテンプレートエンジンである… とジョーク1はさておいて、曲りなりにもテンプレートエンジンなので、<?php … ?>
の外に書かれた文字列は、そのまま出力される。
これは換言すると、次のような説明が成り立つ。
以下の
a.php
とb.php
のコードは実質等価である。
<!DOCTYPE html>
<title>てすと
<p>みなさん
<?php if (1 == 2) { ?>
こんにちは
<?php } else { ?>
さようなら
<?php }
<?php
echo "<!DOCTYPE html>\n<title>てすと\n<p>みなさん\n";
if (1 == 2) {
echo "こんにちは\n";
} else {
echo "さようなら\n";
}
おわかりいただけるだろうか。
PHPと名前空間
先に見せたPHPコマンドの実行結果を再度掲載する。
% echo '<?php namespace dummy; echo "hello", PHP_EOL;' | nkf --oc=UTF-8-BOM | php
Fatal error: Namespace declaration statement has to be the very first statement in the script in - on line 1
致命的エラーのメッセージを良く読んでいただきたい。
また、前回の「モダンPHPアンチパターン」にも引いたPHP: 名前空間 - Manualの子記事であるPHP: 名前空間の定義 - Manualからも一節を引用する。
名前空間を宣言するには、キーワード
namespace
を使用します。名前空間を含むファイルでは、他のコードより前にファイルの先頭で名前空間を宣言しなければなりません。 ただし declare キーワードは例外です。
(php.net - PHP: 名前空間の定義 - Manualより引用 2015年9月24日閲覧)
なるほど、有効なPHPソースコードでは、namespace
定義より前にはdeclare
文しか書くことは許されない。
これまでの記事の説明に従って解釈すれば、ワンライナーのソースコードは次のように変形できる。
[BOM]<?php namespace dummy; echo "hello", PHP_EOL;
<?php
echo "[BOM]";
namespace dummy;
echo "hello", PHP_EOL;
(ここではわかりやすさのために、UTF-8のBOM0xEF 0xBB 0xBF
を[BOM]
と表記した。)
x.php
, y.php
ともに、namespace
よりも前にecho
文が登場してしまった。よって、これらのプログラムはPHPのソースコードとしてSyntax Errorである。
ついでにJSONの話
JSONの最新仕様であるRFC 7159 - The JavaScript Object Notation (JSON) Data Interchange Formatによれば、JSONのSyntaxは以下の通りだ。
2. JSON Grammar
A JSON text is a sequence of tokens. The set of tokens includes six
structural characters, strings, numbers, and three literal names.
A JSON text is a serialized value. Note that certain previous
specifications of JSON constrained a JSON text to be an object or an
array. Implementations that generate only objects or arrays where a
JSON text is called for will be interoperable in the sense that all
implementations will accept these as conforming JSON texts.
JSON-text = ws value ws
(中略)
Insignificant whitespace is allowed before or after any of the six
structural characters.
ws = *(
%x20 / ; Space
%x09 / ; Horizontal tab
%x0A / ; Line feed or New line
%x0D ) ; Carriage return
(後略)
JSONにとってBOMはホワイトスペースではないし、後略したそのほかのリテラルにとって意味のある文字でもない。
次に、同じ文書から文字エンコーディングについての節も抜萃する。
8.1. Character Encoding
JSON text SHALL be encoded in UTF-8, UTF-16, or UTF-32. The default
encoding is UTF-8, and JSON texts that are encoded in UTF-8 are
interoperable in the sense that they will be read successfully by the
maximum number of implementations; there are many implementations
that cannot successfully read texts in other encodings (such as
UTF-16 and UTF-32).
Implementations MUST NOT add a byte order mark to the beginning of a
JSON text. In the interests of interoperability, implementations
that parse JSON texts MAY ignore the presence of a byte order mark
rather than treating it as an error.
まとめると「相互運用性のために推奨するエンコーディングはUTF-8だよ」「JSON処理系はBOMを付加しちゃアカンよ」「JSON処理系は互換性のためにBOMをエラーにしないで無視するかもね」って感じですね。
あるJSON処理系は先頭バイトのBOMをSyntax Errorとして怒るし、別のある処理系は空気を読んで見逃してくれるってことですね。
namespaceの効能(副作用)
上記までの内容を総合することで、UTF-8であるにも拘らずBOMを付加しようとする不届きなエディタで編集されたソースコードはnamespace
を導入することで一律Syntax Errorとみなすことが可能になり、無意味なBOMを先頭バイトに持ったJSONレスポンスを出力することを抑止できます。
BOMを出力しない意味
「そんな、UTF-8にBOMが含まれたってデメリットはないし、プログラマーが気持ち悪いって騒ぐだけなんでしょ」この指摘は半分合ってるし、半分間違ってる。
もしあなたのWebアプリケーションがContent-Type: text/html
だけを返すならば、それは間違ってない。Content-Type: application/json
を返す場合においても、JSONパーサーが空気を読んでくれる場合において実害は発生しない。
ところが、テキストではないファイルを出力する用事に駆られたときはどうだろう。
<?php
header('Content-Type: image/gif');
// UTF-8 BOM
echo json_encode(hex2bin('efbbbf'));
// Base64エンコードされた1x1のspacer.gif
echo base64_decode('R0lGODlhAQABAPAAAP8AAAAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==');
このファイルを保存して php -S localhost:3939
でサーバーを起動し、ブラウザで http://localhost:3939/bomgif.php を開いてみよう。
ファイルの性質によるものの、無意味なBOMを含んだファイルをうまく扱ってくれるバイナリフォーマットの処理系は多くないような気がしてる。
まとめ
自分の靴を舐めるつもりで書いたけど、意外と説明することが多かった。あと一発ギャグを説明することで精神につらさがこみあげてきた。
さて、鎮守府に着任しますか ヾ(〃><)ノ゙☆
-
返すがへすも、これはジョークである ↩