PHPのnl2brは改行を消さずbrを足すだけ htmlspecialcharsとの順番を逆にするとタグが文字で出る理由
nl2brは、文字列中の改行の手前に <br /> を挿入するPHPの関数です。改行コードそのものは消えず、HTMLエスケープも一切しないので、ユーザー入力に使うときは順番に注意が必要です。
掲示板やお問い合わせフォームの確認画面で、入力された複数行のテキストをそのまま表示したい場面は多いですね。そこで反射的にnl2brを使うのですが、htmlspecialcharsとの順番を一度でも間違えると、画面に「<br />」が文字として出たり、逆にタグが素通りしたりします。私も昔、確認画面で生の <br /> が並んでいるのを見て首をかしげた記憶があります。
nl2brは何をして何をしないのか
シグネチャは nl2br(string $string, bool $use_xhtml = true): string です。改行(\n、\r\n、\r)を見つけたら、その手前に <br /> を足します。第2引数に false を渡すと <br> になります。
ポイントは、改行コードを置き換えるのではなく、手前に足すだけという点です。実際に動かすと次のようになります。
<?php
$s = "1行目\n2行目\r\n3行目";
var_dump(nl2br($s));
// string(36) "1行目<br />
// 2行目<br />
// 3行目"
var_dump(nl2br($s, false));
// string(32) "1行目<br>
// 2行目<br>
// 3行目"
改行が残っているので、HTMLソースを見ると読みやすいままです。ブラウザは通常の要素内の改行を空白として扱うので、表示上は <br> だけが効きます。一方で、<pre> の中で使うと改行が二重に効いて、行間が倍になります。
なお、\n\r の並びは1つの改行として扱われ、\r\n と同様に <br /> は1回だけ入ります。\r 単独も改行扱いです。空行(\n\n)は、そのぶん <br /> が連続して入ります。
htmlspecialcharsとの順番はどちらが正しいのか
結論から言うと、先にhtmlspecialchars、後でnl2brです。nl2brが挿入する <br /> は、こちらが意図したHTMLなので、エスケープされては困ります。逆に、ユーザーが入力した <b> などは、タグとして解釈されては困ります。この両立ができるのは、この順番だけです。
<?php
$in = "<b>太字</b>\n次の行 & 続き";
// 正しい順番: エスケープしてから改行をbrにする
var_dump(nl2br(htmlspecialchars($in)));
// string(54) "<b>太字</b><br />
// 次の行 & 続き"
// 逆にすると、brまでエスケープされて文字として見えてしまう
var_dump(htmlspecialchars(nl2br($in)));
// string(60) "<b>太字</b><br />
// 次の行 & 続き"
// nl2brだけだと、入力の<b>がそのままタグとして出力される
var_dump(nl2br($in));
// string(38) "<b>太字</b><br />
// 次の行 & 続き"
上のコメントでは、var_dumpの出力に含まれる文字をそのまま載せています。見た目が紛らわしいですが、2つ目は「<br />」という文字列になっていて、ブラウザには「<br />」というテキストとして表示されます。3つ目はユーザーの <b> が生きたままなので、<script> を入れられればそのままXSSになります。
実務では、毎回この2つを重ねて書くのは面倒なので、小さなヘルパーにまとめておくと事故が減ります。
<?php
function e_multiline(string $text): string
{
return nl2br(htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
}
echo e_multiline("こんにちは\n<script>alert(1)</script>");
ENT_SUBSTITUTE を付けておくと、不正なUTF-8バイト列が混じっていても、空文字ではなく置換文字になります。PHP 8.1以降はデフォルトのフラグに ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 が入っていますが、バージョンをまたぐコードでは明示しておくほうが安心です。
nl2brを使わずCSSで済ませる手もある?
表示専用なら、PHP側で変換せずに、エスケープだけして CSS の white-space: pre-line; を指定する方法もあります。pre-lineは、連続する空白を1つにまとめつつ、改行は改行として表示します。HTMLに <br> が混ざらないので、あとで同じ文字列をメール本文やCSV出力に再利用するときにも扱いやすいです。
ただし、メールのHTML本文のようにCSSが効かない環境や、RSS・外部サービスへ渡すHTMLでは、nl2brのほうが確実です。表示先がどこかで使い分ける、というのが現実的だと思います。
まとめ
nl2brは「改行の手前にbrを足す」だけの関数で、エスケープはしてくれません。ユーザー入力に使うなら、htmlspecialcharsを先に、nl2brを後に。この順番だけ覚えておけば、画面にbrが出る事故も、タグが素通りする事故も避けられます。毎回書くのが面倒なら、ヘルパーにしてしまうのが一番楽かもしれません。
よくある質問
Q. nl2brは改行コードを <br> に置き換えるのですか?
A. 置き換えではなく、改行コードの手前に <br /> を挿入します。改行コード自体は残ります。
Q. <br> と <br /> のどちらで出力されますか?
A. デフォルトでは <br /> です。第2引数に false を渡すと <br> になります。HTML5では、どちらも問題なく扱われます。
Q. nl2brを通した文字列から元の改行に戻せますか?
A. 標準の逆変換関数はありません。元の文字列を別に保持しておくか、保存時は生のテキストで保存して、表示時にだけnl2brを通す設計にするのが安全です。