替换字符串中的变量

时间:2013-08-12 21:58:33

标签: php variables

我正在使用PHP的多语言网站,在我的语言文件中,我经常使用包含多个变量的字符串,这些变量稍后会被填写以完成句子。

目前我将{VAR_NAME}放入字符串中,并在使用时手动将每个出现的匹配值替换掉。

基本上是这样的:

{X} created a thread on {Y}

成为:

Dany created a thread on Stack Overflow

我已经考虑过sprintf,但我发现它不方便,因为它取决于变量的顺序,这些变量可以从一种语言变为另一种语言。

我已经检查了How replace variable in string with value in php?,现在我基本上都使用了这种方法。

但我有兴趣知道在PHP中是否有内置(或可能不是)方便的方法,考虑到我已经在前面的示例中已经将变量命名为X和Y,更像是for变量变量。

所以不要在字符串上执行str_replace,我可能会调用这样的函数:

$X = 'Dany';
$Y = 'Stack Overflow';
$lang['example'] = '{X} created a thread on {Y}';

echo parse($lang['example']);

也会打印出来:

Dany created a thread on Stack Overflow

谢谢!

修改

字符串用作模板,可以使用不同的输入多次使用。

所以基本上做"{$X} ... {$Y}"不会有效,因为我将丢失模板,字符串将初始化为$X$Y的起始值确定。

12 个答案:

答案 0 :(得分:44)

我要在这里添加一个答案,因为在我看来,目前的答案都没有真正削减芥末。我会直接潜入并向您展示我将使用的代码:

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);
}

这是做什么的?

简而言之:

  • 使用指定的转义字符创建正则表达式,该转义字符将匹配三个序列之一(下面有更多内容)
  • 将其输入preg_replace_callback(),其中回调处理其中两个序列,并将其他所有序列视为替换操作。
  • 返回结果字符串

正则表达式

正则表达式匹配这三个序列中的任何一个:

  • 两次出现转义字符,然后出现零次或多次转义字符,后跟一个左大括号。仅消耗转义字符的前两次出现。这将由一次转义字符替换。
  • 单个出现的转义字符后跟一个左大括号。这被一个文字的开放花括号所取代。
  • 一个开口大括号,后跟一个或多个perl字符(alpha-numerics和下划线字符),后跟一个闭合的大括号。这被视为占位符,并且对$variables数组中的大括号之间的名称执行查找,如果找到则返回替换值,如果不是,则返回$errPlaceholder的值 - 默认为null,将其视为特殊情况并返回原始占位符(即不修改字符串)。

为什么会更好?

要理解为什么会更好,让我们看一下其他答案所取代的替代方法。使用one exception(唯一失败的是兼容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,但它实际上是CCdemo) - 这是因为第一次迭代替换了{{1与A一起使用,并且在第二次迭代中,主题字符串为B - 因此BB的这两种情况都被B替换。

这个问题也背叛了可能不会立即显而易见的性能考虑因素 - 因为每一对都是单独处理的,操作是C,对于每个替换对,搜索整个字符串并处理单个替换操作。如果你有一个非常大的主题字符串和很多替换对,这是一个在发动机罩下进行的大规模操作。

可以说这个性能考虑是没有问题的 - 在你有意义的减速之前你需要一个非常大字符串和一个很多的替换对,但它&# 39; s仍然值得记住。同样值得记住的是,正则表达式本身具有性能损失,因此一般来说,这种考虑不应该包含在决策过程中。

相反,我们使用O(n)。这将访问字符串的任何给定部分,在提供的正则表达式的范围内只查找匹配一次。我添加了这个限定符,因为如果你写一个导致catastrophic backtracking的表达式,那么它将不止一次,但在这种情况下,这不应该是一个问题(为了避免这种情况,我做了唯一的重复表达式possessive)。

我们使用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} 应该是文字呢?

@

请注意,正则表达式的设计目的只是注意开头大括号之前的转义序列。这意味着您不需要转义转义字符,除非它立即出现在占位符前面。

关于使用数组作为参数的说明

您的原始代码示例使用的命名方式与字符串中的占位符相同。我使用带有命名键的数组。这有两个很好的理由:

  1. 清晰度和安全性 - 更容易看到最终会被取代的东西,并且您不会冒险意外地替换您不想接触的变量。如果有人可以简单地输入$lang['example'] = '{X} created a thread on {Y} and it contained @@{X}'; // Dany created a thread on Stack Overflow and it contained @Dany 并查看您的数据库密码,现在不是吗?
  2. 范围 - 除非调用者是全局范围,否则无法从调用范围导入变量。如果从另一个函数调用该函数会使该函数无效,并且从另一个作用域导入数据是非常糟糕的做法。
  3. 如果确实想要使用当前作用域中的命名变量(由于前面提到的安全问题,推荐这个),您可以传递结果致电get_defined_vars()第二个论点。

    关于选择转义字符的说明

    您会注意到我选择{dbPass}作为默认转义字符。您可以将任何字符(或字符序列,它可以不止一个)传递给第三个参数 - 您可能会想要使用@,因为这是许多语言使用的,< em>但在你做之前坚持。

    您不想使用\的原因是,因为许多语言将其用作自己的转义字符,这意味着当您想要指定转义字符时比方说,PHP字符串文字,你遇到了这个问题:

    \

    它可能导致可读性噩梦,以及一些复杂模式的非显而易见的行为。选择一个未被任何其他语言使用的转义字符(例如,如果您使用此技术生成HTML片段,请不要使用$lang['example'] = '\\{X}'; // results in {X} $lang['example'] = '\\\{X}'; // results in \Dany $lang['example'] = '\\\\{X}'; // results in \Dany 作为转义字符。)

    总结

    你在做什么有边缘情况。要正确解决问题,您需要使用能够处理这些边缘情况的工具 - 当涉及字符串操作时,该工作的工具通常是正则表达式。

答案 1 :(得分:11)

这是一个使用变量变量的便携式解决方案。耶!

$string = "I need to replace {X} and {Y}";
$X = 'something';
$Y = 'something else';

preg_match_all('/\{(.*?)\}/', $string, $matches);           

foreach ($matches[1] as $value)
{
    $string = str_replace('{'.$value.'}', ${$value}, $string);
}

首先设置字符串和替换字符串。然后,执行正则表达式以获取匹配数组({和}中的字符串,包括那些括号)。最后,使用变量变量循环遍历这些并使用上面创建的变量替换它们。可爱!


即使您已将其标记为正确,我也会想到我会使用其他选项更新此选项。你没有拥有来使用变量变量,并且可以在它的位置使用数组。

$map = array(
    'X' => 'something',
    'Y' => 'something else'
);

preg_match_all('/\{(.*?)\}/', $string, $matches);           

foreach ($matches[1] as $value)
{
    $string = str_replace('{'.$value.'}', $map[$value], $string);
}

这将允许您创建具有以下签名的函数:

public function parse($string, $map); // Probably what I'd do tbh

感谢comments中的工具制作者提出的另一个选项不需要循环并使用strtr,但需要对变量和单引号进行少量添加而不是双引号:

$string = 'I need to replace {$X} and {$Y}';

$map = array(
    '{$X}' => 'something',
    '{$Y}' => 'something else'
);

$string = strtr($string, $map);

答案 2 :(得分:4)

如果你正在运行5.4并且你关心能够在字符串中使用PHP的内置变量插值,你可以使用bindTo()的{​​{1}}方法,如下所示:

Closure

Codepad Demo

也许,感觉有点hacky,我并不特别喜欢在这个例子中使用// Strings use interpolation, but have to return themselves from an anon func $strings = [ 'en' => [ 'message_sent' => function() { return "You just sent a message to $this->recipient that said: $this->message."; } ], 'es' => [ 'message_sent' => function() { return "Acabas de enviar un mensaje a $this->recipient que dijo: $this->message."; } ] ]; class LocalizationScope { private $data; public function __construct($data) { $this->data = $data; } public function __get($param) { if(isset($this->data[$param])) { return $this->data[$param]; } return ''; } } // Bind the string anon func to an object of the array data passed in and invoke (returns string) function localize($stringCb, $data) { return $stringCb->bindTo(new LocalizationScope($data))->__invoke(); } // Demo foreach($strings as $str) { var_dump(localize($str['message_sent'], array( 'recipient' => 'Jeff Atwood', 'message' => 'The project should be done in 6 to 8 weeks.' ))); } //string(93) "You just sent a message to Jeff Atwood that said: The project should be done in 6 to 8 weeks." //string(95) "Acabas de enviar un mensaje a Jeff Atwood que dijo: The project should be done in 6 to 8 weeks." 。但是你确实可以获得依赖PHP变量插值的额外好处(它允许你做一些事情,比如使用正则表达式很难实现的转义)。


编辑已添加$this,这增加了另一个好处:如果本地化匿名函数尝试访问未提供的数据,则不会发出警告。

答案 3 :(得分:2)

对于这类事情,

strtr可能是更好的选择,因为它首先取代了最长的键:

$repls = array(
  'X' => 'Dany',
  'Y' => 'Stack Overflow',
);

foreach($data as $key => $value)
  $repls['{' . $key . '}'] = $value;

$result = strtr($text, $repls);

(想想你有像XX和X这样的键的情况)


如果您不想使用数组,而是公开当前范围内的所有变量:

$repls = get_defined_vars();

答案 4 :(得分:2)

如果您对sprintf的唯一问题是参数的顺序,您可以使用参数交换。

来自doc(http://php.net/manual/en/function.sprintf.php):

$format = 'The %2$s contains %1$d monkeys';
echo sprintf($format, $num, $location);

答案 5 :(得分:2)

gettext 是一种广泛使用的通用本地化系统,可以完全满足您的需求。 大多数编程语言都有库, PHP 内置引擎。 它由po文件驱动,基于简单的文本格式,有许多编辑器,它与sprintf语法兼容。

它甚至还有一些功能来处理某些语言所具有的复杂复数等事物。

以下是它的作用的一些例子。请注意,_()是gettext()的别名:

  • echo _('Hello world'); //将以当前所选语言输出hello world
  • echo sprintf(_("%s has created a thread on %s"), $name, $site); //翻译字符串,然后将其交给sprintf()
  • echo sprintf(_("%2$s has created a thread on %1$s"), $site, $name); //与上述相同,但参数顺序已更改。

如果你有一些以上的字符串,你绝对应该使用现有的引擎,而不是编写自己的引擎。 添加新语言只需翻译字符串列表,大多数专业翻译工具也可以使用此文件格式。

检查Wikipedia和PHP文档,了解其工作原理的基本概述:

Google发现大量文档和您最喜欢的软件存储库很可能会有一些管理po文件的工具。

我使用过的一些是:

  • poedit :非常简单明了。很好,如果你没有太多的东西要翻译,不想花时间思考这些东西是如何工作的。
  • Virtaal :有点复杂,有一点学习曲线,但也有一些很好的功能,让您的生活更轻松。好的,如果你需要翻译很多。
  • GlotPress 是一个Web应用程序(来自wordpress人员),允许协作编辑翻译数据库文件。

答案 6 :(得分:1)

为什么不使用str_replace呢?如果你想把它作为模板。

echo str_replace(array('{X}', '{Y}'), array($X, $Y), $lang['example']);

对于您需要的每次出现

str_replace首先是为此而构建的。

答案 7 :(得分:0)

简单:

$X = 'Dany';
$Y = 'Stack Overflow';
$lang['example'] = "{$X} created a thread on {$Y}";

因此:

echo $lang['example'];

将输出:

Dany created a thread on Stack Overflow

按照你的要求。

<强>更新

根据OP关于使解决方案更具便携性的评论:

让班级每次都为你做解析:

class MyParser {
  function parse($vstr) {
    return "{$x} created a thread on {$y}";
  }
}

这样,如果发生以下情况:

$X = 3;
$Y = 4;

$a = new MyParser();
$lang['example'] = $a->parse($X, $Y);

echo $lang['example'];

将返回:

3 created a thread on 4;

并且,仔细检查:

$X = 'Steve';
$Y = 10.9;

$lang['example'] = $a->parse($X, $Y);

将打印:

Steve created a thread on 10.9;

根据需要。

更新2:

根据OP关于提高便携性的评论:

class MyParser {
  function parse($vstr) {
    return "{$vstr}";
  }
}

$a = new MyParser();

$X = 3;
$Y = 4;
$vstr = "{$X} created a thread on {$Y}";

$a = new MyParser();
$lang['example'] = $a->parse($vstr);

echo $lang['example'];

将输出之前引用的结果。

答案 8 :(得分:0)

尝试

$lang['example'] = "$X created a thread on $Y";

编辑:基于最新信息

也许您需要查看sprintf()函数

然后您可以将模板字符串定义为此

$template_string = '%s created a thread on %s';


$X = 'Fred';
$Y = 'Sunday';

echo sprintf( $template_string, $X, $Y );

$template_string不会更改,但是当您为$X$Y指定了不同的值后,您的代码中的后续内容仍然可以使用echo sprintf( $template_string, $X, $Y );

See PHP Manual

答案 9 :(得分:0)

如何将“变量”部分定义为一个数组,其中的键对应于字符串中的占位符?

$string = "{X} created a thread on {Y}";
$values = array(
   'X' => "Danny",
   'Y' => "Stack Overflow",
);

echo str_replace(
   array_map(function($v) { return '{'.$v.'}'; }, array_keys($values)),
   array_values($values),
   $string
);

答案 10 :(得分:0)

为什么不能在函数中使用模板字符串?

function threadTemplate($x, $y) {
    return "{$x} created a thread on {$y}";
}
echo threadTemplate($foo, $bar);

答案 11 :(得分:0)

在使用关联数组时抛出另一个解决方案。这将遍历关联数组,并替换模板或将其留空。

示例:

$list = array();
$list['X'] = 'Dany';
$list['Y'] = 'Stack Overflow';

$str = '{X} created a thread on {Y}';

$newstring = textReplaceContent($str,$list);


    function textReplaceContent($contents, $list) {


                while (list($key, $val) = each($list)) {
                    $key = "{" . $key . "}";
                    if ($val) {
                        $contents = str_replace($key, $val, $contents);
                    } else {
                        $contents = str_replace($key, "", $contents);
                    }
                }
                $final = preg_replace('/\[\w+\]/', '', $contents);

                return ($final);
            }