str_replaceを配列で使うときの注意


str_replace - 検索文字列に一致したすべての文字列を置換する

説明

mixed str_replace  ( mixed $search  , mixed $replace  , mixed $subject  [, int &$count  ] )

この関数は、subject の中の search を全て replace に置換します。

(中略)

search と replace が配列の場合、str_replace() は各配列から値をひとつ取り出し、 subject 上で検索と置換を行うために使用します。 replace の値が search よりも少ない場合、 置換される値の残りの部分には空の文字列が使用されます。 search が配列で replace が文字列の場合、この置換文字列が search の各値について使用されます。しかし、 逆は意味がありません。

search あるいは replace が配列の場合は、配列の最初の要素から順に処理されます。

PHPのstr_replaceはいずれの引数も配列を指定できるようになっていて、まとめて置換処理ができるんだけど、searchとreplaceに配列を渡すとちょうどtrの文字列版のように変換することができる。
対応表でまとめて置換したいような場合に便利だ。


しかし、こんな罠が潜んでいたりするので注意も必要。

<?php
$map = array(
    'foo' => 'bar',
    'bar' => 'baz',
    'baz' => 'quux',
);

$str = 'foo bar baz quux';
echo str_replace(array_keys($map), array_values($map), $str) . "\n";
?>
quux quux quux quux

/(^o^)\


実際の実装は確認してないが、どうやらstr_replaceは単純に各要素をぐるぐる回しながら順に置換しているようだ。
ようするにこんな感じ。

  1. 'foo' => 'bar'の置換で'bar bar baz quux'になる。
  2. 'bar' => 'baz'の置換で'baz baz baz quux'になる。
  3. 'baz' => 'quux'の置換で'quux quux quux quux'になる。


マニュアルの「search あるいは replace が配列の場合は、配列の最初の要素から順に処理されます。」というのはこのことだったのだ。

1-passで処理する

これを回避するには頭から1回の走査で置換すればよい。

<?php
$map = array(
    'foo' => 'bar',
    'bar' => 'baz',
    'baz' => 'quux',
);

$str = 'foo bar baz quux';
echo my_str_replace($map, $str) . "\n";

function my_str_replace($map, $str) {
    $list = array();
    foreach (array_keys($map) as $key) {
        $list[] = preg_quote($key);
    }
    $pattern = '/(' . implode('|', $list) . ')/ue';
    return preg_replace($pattern, '$map[\'$1\']', $str);
}
?>
bar baz quux quux

上記のコードは簡略化してあるが、こんな感じで端から順に複数のsearchを検索して、見つけたら対応するものに置換すればよいかと。


ちなみに'$map[\'$1\']'のところは$1にシングルクォートが入ってたらどうなるんだろうと思って試してみたけど、何事もなく正常に処理された。
'$map[$1]'にしたらbare-wordになるようで、undefined constantとかって怒られた。よくわからん。
なんとなく怖いので実際に使うときはpreg_replace_callbackで実装するのがよいと思われ。


ちなみにcreate_functionは使わない派。