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 が配列の場合は、配列の最初の要素から順に処理されます。
(from PHP: Hypertext Preprocessor)
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は単純に各要素をぐるぐる回しながら順に置換しているようだ。
ようするにこんな感じ。
- 'foo' => 'bar'の置換で'bar bar baz quux'になる。
- 'bar' => 'baz'の置換で'baz baz baz quux'になる。
- '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は使わない派。