我将在这里添加一个答案,因为在我看来,当前的答案都没有真正切中要害。我将直接潜入并向您展示我将用于执行此操作的代码:
function parse(
/* string */ $subject,
array $variables,
/* string */ $escapeChar = '@',
/* string */ $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 的值 - 默认为 @ 987654334@,作为特例处理,返回原始占位符(即不修改字符串)。
为什么更好?
要了解为什么它更好,让我们看看其他答案所采用的替代方法。对于one exception(唯一的缺点是与 PHP
-
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) - 这是因为第一次迭代将 A 替换为 B,并且在第二次迭代,主题字符串是BB - 所以B 的这两次出现都被C 替换。
这个问题还暴露了一个可能不会立即明显的性能考虑 - 因为每一对都是单独处理的,操作是 O(n),对于每个替换对,整个字符串都被搜索并处理单个替换操作。如果您有一个非常大的主题字符串和很多替换对,那么这就是在引擎盖下进行的一项相当大的操作。
可以说,这种性能考虑不是问题 - 您需要一个 very 大字符串和一个 lot 替换对,然后才能获得有意义的减速,但它仍然存在值得记住。还值得记住的是,正则表达式有其自身的性能损失,因此一般而言,这种考虑不应包含在决策过程中。
我们使用preg_replace_callback()。这将访问字符串的任何给定部分,在提供的正则表达式的范围内只查找一次匹配。我添加了这个限定符,因为如果你编写一个导致catastrophic backtracking 的表达式,那么它将不止一次,但在这种情况下这不应该是一个问题(为了帮助避免这种情况,我在表达式@987654327 中做了唯一的重复@)。
我们使用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);
// Dany created a thread on Stack Overflow
更高级的东西
现在假设我们有:
$lang['example'] = '{X} created a thread on {Y} and it contained {X}';
// Dany created a thread on Stack Overflow and it contained Dany
...我们希望第二个{X} 出现在结果字符串中字面意思。使用 @ 的默认转义字符,我们将其更改为:
$lang['example'] = '{X} created a thread on {Y} and it contained @{X}';
// Dany created a thread on Stack Overflow and it contained {X}
好的,到目前为止看起来不错。但是如果 @ 应该是文字呢?
$lang['example'] = '{X} created a thread on {Y} and it contained @@{X}';
// Dany created a thread on Stack Overflow and it contained @Dany
请注意,正则表达式被设计为仅关注紧接在左大括号之前的转义序列。这意味着您不需要转义转义字符,除非它立即出现在占位符前面。
关于使用数组作为参数的说明
您的原始代码示例使用的变量命名方式与字符串中的占位符相同。我的使用带有命名键的数组。这有两个很好的理由:
- 清晰性和安全性 - 更容易看到最终将被替换的内容,并且您不会冒着意外替换您不想暴露的变量的风险。如果有人可以简单地输入
{dbPass} 并查看您的数据库密码,那不是很好,不是吗?
- 范围 - 除非调用者是全局范围,否则无法从调用范围导入变量。如果从另一个函数调用,这会使该函数无用,并且从另一个范围导入数据是非常糟糕的做法。
如果您真的想使用当前范围内的命名变量(由于上述安全问题,我确实不推荐这样做),您可以传递调用 get_defined_vars() 到第二个参数。
关于选择转义字符的注意事项
您会注意到我选择了@ 作为默认转义字符。您可以通过将其传递给第三个参数来使用任何字符(或字符序列,它可以不止一个) - 您可能很想使用\,因为这是许多语言使用的,但在此之前坚持你这样做。
您不想使用\ 的原因是因为许多语言都使用它作为自己的转义字符,这意味着当您想指定转义字符时,比如说, PHP字符串文字,你遇到了这个问题:
$lang['example'] = '\\{X}'; // results in {X}
$lang['example'] = '\\\{X}'; // results in \Dany
$lang['example'] = '\\\\{X}'; // results in \Dany
它可能导致可读性噩梦,以及一些具有复杂模式的不明显行为。选择一个不被任何其他语言使用的转义字符(例如,如果您使用此技术生成 HTML 片段,也不要使用 & 作为转义字符)。
总结一下
您正在做的事情有极端情况。要正确解决问题,您需要使用能够处理这些极端情况的工具——当涉及到字符串操作时,最常用的工具是正则表达式。