コーディングTips

見出しの中の「$5」が消えた話、preg_replaceで同じ症状を再現できた

公開 約5分で読めます

目次5項目
  1. 症状: 見出しだけ「$5」が消える
  2. 最小コードで再現してみる
  3. なぜ最小コードでは「$5」が消えたのか
  4. 暫定の対処は「5ドル」表記に変更
  5. まとめ

見出しに「$5の無料トライアル」って書いて公開確認したら、見出しから「$5」の部分だけが消えていました。
本文中の<p>タグでは同じ「$5」が普通に表示されていたので、見出しタグ限定の現象。
Cocoon側の該当コードまでは追い切れていませんが、同じ症状を最小コードで再現できたので、そこまでの過程を書きます。

症状: 見出しだけ「$5」が消える

WordPress公式のAI連携を試した記事を書いていたときの話です。
見出しに「$5の無料トライアルで止まった話」と書いて保存し、実際のページを開いたら「の無料トライアルで止まった話」になっていました。
最初はコピペミスを疑いました。
違いました。
DBの中身を直接見て、それも違うと分かったからです。

wp post get 797 --field=post_content | grep "無料トライアル"
# → 見出しに $5の無料トライアルで止まった話 と、ちゃんと入っている

DB上のpost_contentには「$5」がそのまま残っています。
消えているのは画面表示だけ。
つまり保存時ではなく、表示するときの処理のどこかで消されているという切り分けができました。
本文の<p>タグ内にある「$5」はまったく無事で、消えていたのは<h2>タグの中だけでした。

最小コードで再現してみる

使っているCocoonというテーマは、見出しから目次(TOC)を自動生成する機能を持っています。
具体的な該当コードまでは特定できていませんが、見出しのテキストをいったん取り出して、新しいHTML文字列に組み立て直していそうだと推測しました。
そこで、その推測だけを使って最小構成のコードを書き、同じ症状が起きるか試しました。

$content = "<h2>\$5の無料トライアルで止まった話</h2>";

// 見出しのテキストを取り出して、置換用の文字列に直接連結する
preg_match('/<h2>(.*?)<\/h2>/', $content, $m);
$heading = $m[1];
$replacement = '<h2><span id="toc1">' . $heading . '</span></h2>';

echo preg_replace('/<h2>(.*?)<\/h2>/', $replacement, $content);
// → <h2><span id="toc1">の無料トライアルで止まった話</span></h2>
// 「$5」だけ消えている

手元でこのコードを実行したら、まったく同じ症状が再現。
Cocoon本体の実装がこの通りかどうかまでは確認できていませんが、症状を作り出す一般的なメカニズムは、これで説明がつきます。
最初は$1のようなバックリファレンスが再帰的に解釈されているのだと思っていました。
そこは自分の勘違いでした。
実際に起きているのは、もっと手前の話でした。

なぜ最小コードでは「$5」が消えたのか

preg_replaceの第2引数(置換文字列)は、どうやって組み立てられたかに関係なく、PHP公式マニュアルにある通り、渡された時点で$0・$1のようなパターンをすべてバックリファレンスとして走査します。
今回の例では、見出しのテキスト($heading)を連結して作った$replacementがpreg_replaceの第2引数として処理される際、その中に含まれていた「$5」もバックリファレンスとして解釈されます。
正規表現側のキャプチャグループは1つしかないので、5個目は存在しません。
存在しない参照は空文字。だからその部分が消えます。

本文の<p>タグで同じ「$5」が無事だった理由もこれで説明がつきます。
少なくとも今回の表示では、本文の<p>内にある「$5」は消えず、見出し内だけで症状が起きました。
見出しだけが同様の組み立て処理を通っていたと仮定すれば、この違いも説明できます。

💡 Tips: 安全なのはpreg_replace_callback

この事故が起きるのは、preg_replaceの置換文字列に、$5のようなバックリファレンス形式を含み得るテキストを直接連結したときです。今回の場合、そのテキストが見出しの中身でした。
preg_replace_callbackを使えば、コールバック関数の戻り値としてマッチ文字列を返すだけなので、この種の事故は起きません。

$content = "<h2>\$5の無料トライアルで止まった話</h2>";

$result = preg_replace_callback(
	'/<h2>(.*?)<\/h2>/',
	static function ($matches) {
		return '<h2><span id="toc1">'
			. $matches[1]
			. '</span></h2>';
	},
	$content
);

echo $result;
// → <h2><span id="toc1">$5の無料トライアルで止まった話</span></h2>
// 「$5」が消えずに残る

暫定の対処は「5ドル」表記に変更

Cocoon側の該当処理を正確に特定して直すには、もう少し調査が必要そうでした。
今回は時間を優先して、見出し側の表記を変える暫定回避で対応しました。
「$5の無料トライアル」を「5ドルの無料トライアル」に書き換えただけです。

// before: 見出し内で消える
<h2>$5の無料トライアルで止まった話</h2>

// after: 暫定回避、ドル記号を使わない
<h2>5ドルの無料トライアルで止まった話</h2>

本文中であれば$5のままでも問題は起きないので、書き換えたのは見出しの部分だけです。
根本原因のテーマ側コードを追いかけるよりも、自分の書き方を1箇所変える方が、その場では早い判断でした。

まとめ

見出しの中身が消えるという症状を見たとき、最初に疑ったのはコピペミスとキャッシュでした。
今回の表示崩れと同じ症状は、preg_replaceの置換文字列に外部テキストを直接連結するコードで再現できました。
最小コードで再現できて、初めて腹落ちしました。
見出しに金額や記号を書くときは、この事故をちょっと思い出してもらえたらうれしいです。

この記事が参考になったら、応援クリックをお願いします

Cat's Handこの記事を書いた人

Web制作歴10年以上。WordPressのテーマカスタマイズやコーディングを中心に、副業・本業ともにweb制作に携わっています。このブログでは、実際に手を動かして分かったことを書いています。