20

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

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

所以基本上:

{X} created a thread on {Y}

变成:

Dany created a thread on Stack Overflow

我已经想到了,sprintf但我觉得这很不方便,因为它取决于变量的顺序,这些变量可以从一种语言变为另一种语言。

而且我已经检查了如何用php中的值替换字符串中的变量?现在我基本上使用这种方法。

但是我很想知道 PHP 中是否有内置的(或者可能没有)方便的方法来做到这一点,因为在前面的示例中我已经有完全命名为 X 和 Y 的变量,更像是 $$ 用于变量变量.

因此,我可能会调用这样的函数,而不是对字符串执行 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

4

12 回答 12

45

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

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 单词字符(字母数字和下划线字符),然后是一个右大括号。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 ) - 这是因为第一次迭代替换AB,而在第二次迭代中主题字符串是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

请注意,正则表达式被设计为仅关注紧接在左大括号之前的转义序列。这意味着您不需要转义转义字符,除非它立即出现在占位符前面。

关于使用数组作为参数的注意事项

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

  1. 清晰性和安全性 - 更容易看到最终将被替换的内容,并且您不会冒着意外替换您不想暴露的变量的风险。如果有人可以简单地输入{dbPass}并查看您的数据库密码,那将不是很好,现在可以吗?
  2. 范围 - 除非调用者是全局范围,否则无法从调用范围导入变量。如果从另一个函数调用,这使得该函数无用,并且从另一个范围导入数据是非常糟糕的做法。

如果您真的想使用当前范围内的命名变量(由于上述安全问题,我get_defined_vars()建议这样做),您可以将调用结果传递给第二个参数。

关于选择转义字符的说明

您会注意到我选择@了默认转义字符。您可以通过将其传递给第三个参数来使用任何字符(或字符序列,它可以不止一个) - 您可能很想使用\,因为这是许多语言使用的,但在您这样做之前请坚持住

您不想使用的原因\因为许多语言将其用作自己的转义字符,这意味着当您想在 PHP 字符串文字中指定转义字符时,您会遇到以下问题:

$lang['example'] = '\\{X}';   // results in {X}
$lang['example'] = '\\\{X}';  // results in \Dany
$lang['example'] = '\\\\{X}'; // results in \Dany

它可能导致可读性噩梦,以及一些具有复杂模式的不明显行为。选择一个不被任何其他语言使用的转义字符(例如,如果您使用此技术生成 HTML 片段,也不要&用作转义字符)。

总结一下

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

于 2013-08-13T15:47:43.410 回答
12

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

$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

感谢 toolmakersteve 在评论中的另一个选项不需要循环并使用strtr,但需要对变量和单引号而不是双引号进行少量添加:

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

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

$string = strtr($string, $map);
于 2013-08-12T22:32:22.937 回答
4

如果您正在运行 5.4 并且您关心能够在字符串中使用 PHP 的内置变量插值,则可以使用如下bindTo()方法Closure

// 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."

键盘演示

也许,感觉有点hacky,我不是特别喜欢$this在这种情况下使用。但是你确实获得了依赖 PHP 的变量插值的额外好处(它允许你做一些事情,比如转义,这是用正则表达式很难实现的)。


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

于 2013-08-12T23:21:37.300 回答
2

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

从文档(http://php.net/manual/en/function.sprintf.php):

$format = 'The %2$s contains %1$d monkeys';
echo sprintf($format, $num, $location);
于 2014-01-13T11:27:00.087 回答
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();
于 2013-08-12T22:34:10.717 回答
2

gettext是一种广泛使用的通用本地化系统,可以完全满足您的需求。大多数编程语言都有库,PHP有一个内置的引擎。它由 po-files 驱动,基于简单文本的格式,周围有许多编辑器,它与 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 文档以获取有关其工作原理的基本概述:

谷歌发现了大量的文档,你最喜欢的软件存储库很可能会有一些用于管理 po-files 的工具。

我用过的一些是:

  • poedit:非常轻巧简单。如果您没有太多要翻译的东西并且不想花时间思考这些东西是如何工作的,那就太好了。
  • Virtaal:稍微复杂一点,有一点学习曲线,但也有一些不错的功能,可以让你的生活更轻松。如果您需要大量翻译,那就太好了。
  • GlotPress是一个 Web 应用程序(来自 wordpress 人员),它允许对翻译数据库文件进行协作编辑。
于 2014-01-13T11:43:36.950 回答
1

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

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

对于您需要的每次发生这种情况

str_replace 最初就是为此而构建的。

于 2013-08-12T22:17:32.073 回答
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
);
于 2013-08-12T22:21:05.650 回答
0

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

function threadTemplate($x, $y) {
    return "{$x} created a thread on {$y}";
}
echo threadTemplate($foo, $bar);
于 2013-08-12T22:36:50.417 回答
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'];

将输出之前引用的结果。

于 2013-08-12T22:01:54.810 回答
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 );

见 PHP 手册

于 2013-08-12T22:02:22.547 回答
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);
            }
于 2014-01-15T19:28:48.653 回答