我将在这里添加一个答案,因为在我看来,当前的答案都没有真正削减芥末。我将直接潜入并向您展示我将用来执行此操作的代码:
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 单词字符(字母数字和下划线字符),然后是一个右大括号。this 被视为占位符,并查找数组中大括号之间的名称
$variables
,如果找到则返回替换值,如果没有则返回值$errPlaceholder
- 默认情况下 this is null
,这被视为特殊大小写并返回原始占位符(即未修改字符串)。
为什么更好?
要了解为什么它更好,让我们看看其他答案所采用的替代方法。除了一个例外(唯一的失败是与 PHP<5.4 的兼容性和稍微不明显的行为),它们分为两类:
strtr()
- 这没有提供处理转义字符的机制。如果您的输入字符串需要文字{X}
怎么办?strtr()
不考虑这一点,它将被 value 取代$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)
,对于每个替换对,搜索整个字符串并处理单个替换操作。如果您有一个非常大的主题字符串和很多替换对,那么这就是在引擎盖下进行的一项相当大的操作。
可以说,这种性能考虑不是问题——你需要一个非常大的字符串和大量的替换对,然后才能得到有意义的减速,但它仍然值得记住。还值得记住的是,正则表达式有其自身的性能损失,因此一般而言,这种考虑不应包含在决策过程中。
相反,我们使用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);
// 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 片段,也不要&
用作转义字符)。
总结一下
你正在做的事情有边缘情况。要正确解决问题,您需要使用能够处理这些边缘情况的工具——当涉及到字符串操作时,该工作的工具通常是正则表达式。