我会在这里添加一个答案,因为我认为当前的答案都不太合适。我将直接展示我用来实现此操作的代码:
function parse(
$subject,
array $variables,
$escapeChar = '@',
$errPlaceholder = null
) {
$esc = preg_quote($escapeChar);
$expr = "/
$esc$esc(?=$esc*+{)
| $esc{
| {(\w+)}
/x";
$callback = function($match) use($variables, $escapeChar, $errPlaceholder) {
switch ($match[0]) {
case $escapeChar . $escapeChar:
return $escapeChar;
case $escapeChar . '{':
return '{';
default:
if (isset($variables[$match[1]])) {
return $variables[$match[1]];
}
return isset($errPlaceholder) ? $errPlaceholder : $match[0];
}
};
return preg_replace_callback($expr, $callback, $subject);
}
那个函数是干什么用的?
简而言之:
正则表达式
正则表达式将匹配以下三个序列中的任何一个:
- 两个转义字符的出现,后跟零个或多个转义字符的出现,后跟一个左花括号。只有前两个转义字符的出现被消耗。这将被替换为一个单独的转义字符。
- 一个转义字符的出现,后跟一个左花括号。这将被替换为一个字面上的左花括号。
- 一个左花括号,后跟一个或多个Perl单词字符(字母数字和下划线字符),后跟一个右花括号。这被视为一个占位符,并在
$variables
数组中查找花括号之间的名称,如果找到,则返回替换值,否则返回$errPlaceholder
的值。默认情况下,这是null
,它被视为特殊情况,将返回原始占位符(即不修改字符串)。
为什么它更好?
要了解为什么它更好,请看其他答案采用的替换方法。除了一个例外(其失败只是与PHP<5.4兼容性和稍微不太明显的行为),它们分为两类:
strtr()
- 它不提供处理转义字符的机制。 如果您的输入字符串需要字面上的{X}
怎么办? strtr()
没有考虑到这一点,它会被替换为值$X
。
str_replace()
- 它存在与strtr()
相同的问题,以及另一个问题。当您使用搜索/替换参数的数组参数调用str_replace()
时,它的行为就好像多次调用它一样-每个替换对数组都调用一次。这意味着如果您的替换字符串之一包含在搜索数组中稍后出现的值,则最终将替换该字符串。
为了演示str_replace()
的此问题,请考虑以下代码:
$pairs = array('A' => 'B', 'B' => 'C');
echo str_replace(array_keys($pairs), array_values($pairs), 'AB');
现在你可能期望这里的输出是BC
,但实际上它将会是CC
(demo) - 这是因为第一个迭代用B
替换了A
,而在第二个迭代中主题字符串是BB
- 所以这两个B
的出现都被替换为C
。
这个问题还揭示了一个性能考虑,这可能不是显而易见的 - 因为每对替换都是单独处理的,所以操作是O(n)
,对于每个替换对,整个字符串都会被搜索并处理单个替换操作。如果你有一个非常大的主题字符串和很多替换对,那么就会在引擎盖下进行一个相当大的操作。
可以说,这种性能考虑是无关紧要的 - 你需要一个非常大的字符串和很多的替换对才能得到有意义的减速,但仍然值得记住。同时也值得记住,正则表达式本身也有性能惩罚,因此通常不应该将此考虑因素纳入决策过程中。
相反,我们使用preg_replace_callback()
。它可以访问任何给定的字符串部分一次,寻找与提供的正则表达式匹配的内容。我添加了这个限定符,因为如果你写一个导致灾难性回溯的表达式,那么它将会超过一次,但在这种情况下,这不应该是一个问题(为了避免这种情况,我在表达式中仅有的重复使用了占有量词)。
我们使用preg_replace_callback()
而不是preg_replace()
,以便我们在查找替换字符串时应用自定义逻辑。
这使你能够做到什么
来自问题的原始示例
$X = 'Dany';
$Y = 'Stack Overflow';
$lang['example'] = '{X} created a thread on {Y}';
echo parse($lang['example']);
这将变成:
$pairs = array(
'X' = 'Dany',
'Y' = 'Stack Overflow',
);
$lang['example'] = '{X} created a thread on {Y}';
echo parse($lang['example'], $pairs);
更高级的某些内容
现在假设我们有:
$lang['example'] = '{X} created a thread on {Y} and it contained {X}';
...而我们希望第二个 {X}
字符串中也能被字面地显示出来。使用默认的转义字符 @
,我们需要改为:
$lang['example'] = '{X} created a thread on {Y} and it contained @{X}';
好的,到目前为止看起来不错。但是如果那个@
符号本应该是一个字面量呢?
$lang['example'] = '{X} created a thread on {Y} and it contained @@{X}';
请注意,正则表达式被设计成只关注紧接着左花括号的转义序列。这意味着你不需要转义转义字符,除非它出现在占位符的前面。
关于使用数组作为参数的说明
原始代码示例使用了与字符串中占位符同名的变量。我的代码使用了具有命名键的数组,这样做有两个非常好的理由:
- 清晰性和安全性——这样能更容易地看到最终将被替换的内容,而且你不会冒意外替换你不想暴露的变量的风险。如果有人可以简单地输入
{dbPass}
并查看你的数据库密码,那就没什么用了,对吧? - 范围——除非调用者是全局范围,否则无法从调用范围导入变量。如果从另一个函数调用该函数,则该函数无用,并且从另一个范围导入数据是非常糟糕的实践。
如果你真的想使用当前范围内的命名变量(由于上述安全问题,我不建议这样做),你可以将 get_defined_vars()
的调用结果传递给第二个参数。
选择转义字符的说明
你会注意到我选择了 @
作为默认的转义字符。你可以使用任何字符(或字符序列,可以是多个),只需将其传递给第三个参数——你可能会想使用 \
因为许多语言都使用它,但在这样做之前先等一下。
不要使用 \
的原因是因为许多语言把它作为自己的转义字符,这意味着当你想在 PHP 字符串字面值中指定你的转义字符时,你会遇到这个问题:
$lang['example'] = '\\{X}';
$lang['example'] = '\\\{X}';
$lang['example'] = '\\\\{X}';
这可能会导致阅读上的噩梦,以及在处理复杂模式时出现一些不明显的行为。选择一个未被任何其他相关语言使用的转义字符(例如,如果你正在使用这种技术来生成HTML片段,则不要使用&
作为转义字符)。
总之
你所做的事情有边缘情况。要正确解决问题,您需要使用能够处理这些边缘情况的工具——而在字符串操作方面,处理该任务的工具通常是正则表达式。