将 preg_replace() 函数中的 e 修饰符替换为 preg_replace_callback()。

87

我不擅长正则表达式。我正在尝试替换这个:

public static function camelize($word) {
   return preg_replace('/(^|_)([a-z])/e', 'strtoupper("\\2")', $word);
}

使用preg_replace_callback和匿名函数进行替换。我不理解\2是做什么的,也不知道preg_replace_callback如何工作。

实现这个功能的正确代码是什么?


2
自 PHP 5.5.0 起,e 修饰符已被弃用 - HamZa
9
我理解。这就是我想用 preg_replace_callback 替换它的原因之一。 - Casey
2
有一个关于 preg_replace_callback 的手册页面。在该回调函数中,\\2 将变成 $matches[2]。或者你具体对哪一部分感到困惑? - mario
1
@mario 啊,$matches[2] 就是我需要的。我仍然不明白它是如何工作的,但它确实有效。如果你把它放在答案里,我会标记它为解决问题的方法。 - Casey
3
请不要使用create_function,它只是eval的另一个包装器。除非你因某种原因被困在PHP 5.2中,否则应该使用适当的匿名函数。 - IMSoP
我得到了 Undefined variable: matches - t q
3个回答

80
在正则表达式中,您可以使用 (括号) “捕获” 匹配字符串的部分;在这种情况下,您正在捕获匹配中的 (^|_)([a-z]) 部分。它们从1开始编号,因此您有反向引用1和2。匹配0是整个匹配的字符串。 /e 修饰符接受替换字符串,并将反斜杠后跟数字(例如 \1)用适当的反向引用替换 - 但由于您在字符串内部,因此需要转义反斜杠,因此您会得到 '\\1'。然后(实际上)运行 eval 将生成的字符串作为 PHP 代码运行(这就是为什么它被弃用的原因,因为易于以不安全的方式使用 eval)。

preg_replace_callback函数采用回调函数,将匹配的反向引用传递给它。因此,您原本会写成'\\1'的地方,您现在要访问该参数的元素1 - 例如,如果您有一个形如function($matches) { ... }的匿名函数,则第一个反向引用是该函数内的$matches[1]

因此,/e参数变为

'do_stuff(\\1) . "and" . do_stuff(\\2)'

可以成为回调函数

function($m) { return do_stuff($m[1]) . "and" . do_stuff($m[2]); }

或者在你的情况下

'strtoupper("\\2")'

可能成为

function($m) { return strtoupper($m[2]); }

请注意,$m$matches不是魔术名称,它们只是我在声明回调函数时给定的参数名称。此外,您不必传递匿名函数,它可以是一个函数名作为字符串,或者类似于array($object, $method)的形式,与PHP中的任何回调一样,例如:
function stuffy_callback($things) {
    return do_stuff($things[1]) . "and" . do_stuff($things[2]);
}
$foo = preg_replace_callback('/([a-z]+) and ([a-z]+)/', 'stuffy_callback', 'fish and chips');

与任何函数一样,默认情况下无法访问回调外部(周围作用域)的变量。当使用匿名函数时,您可以使用use关键字导入需要访问的变量,如PHP手册中所述。例如,如果旧参数为

'do_stuff(\\1, $foo)'

如果是这样,新的回调函数可能会像这样。
function($m) use ($foo) { return do_stuff($m[1], $foo); }

注意事项

  • preg_replace_callback的使用是代替正则表达式中的/e修饰符,因此需要从"pattern"参数中删除该标志。因此,像/blah(.*)blah/mei这样的模式将变为/blah(.*)blah/mi
  • /e修饰符在参数上内部使用了addslashes()的变体,因此一些替换使用stripslashes()进行了删除;在大多数情况下,您可能希望从新的回调中删除对stripslashes的调用。

2

使用带有eval支持的preg_replace替换函数

这非常不可取。但是,如果您不是程序员,或者真的喜欢糟糕的代码,您可以使用一个替代的preg_replace函数来暂时保持您的/e标志工作。

/**
 * Can be used as a stopgap shim for preg_replace() calls with /e flag.
 * Is likely to fail for more complex string munging expressions. And
 * very obviously won't help with local-scope variable expressions.
 *
 * @license: CC-BY-*.*-comment-must-be-retained
 * @security: Provides `eval` support for replacement patterns. Which
 *   poses troubles for user-supplied input when paired with overly
 *   generic placeholders. This variant is only slightly stricter than
 *   the C implementation, but still susceptible to varexpression, quote
 *   breakouts and mundane exploits from unquoted capture placeholders.
 * @url: https://dev59.com/eGUp5IYBdhLWcg3wLlRV
 */
function preg_replace_eval($pattern, $replacement, $subject, $limit=-1) {
    # strip /e flag
    $pattern = preg_replace('/(\W[a-df-z]*)e([a-df-z]*)$/i', '$1$2', $pattern);
    # warn about most blatant misuses at least
    if (preg_match('/\(\.[+*]/', $pattern)) {
        trigger_error("preg_replace_eval(): regex contains (.*) or (.+) placeholders, which easily causes security issues for unconstrained/user input in the replacement expression. Transform your code to use preg_replace_callback() with a sane replacement callback!");
    }
    # run preg_replace with eval-callback
    return preg_replace_callback(
        $pattern,
        function ($matches) use ($replacement) {
            # substitute $1/$2/… with literals from $matches[]
            $repl = preg_replace_callback(
                '/(?<!\\\\)(?:[$]|\\\\)(\d+)/',
                function ($m) use ($matches) {
                    if (!isset($matches[$m[1]])) { trigger_error("No capture group for '$m[0]' eval placeholder"); }
                    return addcslashes($matches[$m[1]], '\"\'\`\$\\\0'); # additionally escapes '$' and backticks
                },
                $replacement
            );
            # run the replacement expression
            return eval("return $repl;");
        },
        $subject,
        $limit
    );
}

实质上,你只需将该函数包含在你的代码库中,并在使用/e标志的任何地方将preg_replace修改为preg_replace_eval优点和缺点:
  • 仅在Stack Overflow的一些示例中进行了测试。
  • 仅支持简单情况(函数调用,而非变量查找)。
  • 包含一些限制和建议性通知。
  • 对于表达式失败,将产生错位且不太可理解的错误。
  • 然而,它仍是一个可用的临时解决方案,并不会给preg_replace_callback的适当转换带来复杂性。
  • 许可证注释只是为了防止人们过度使用或扩散这个东西。

替换代码生成器

现在这有点多余。但可能有助于那些仍然被手动重构他们的代码preg_replace_callback所压倒。虽然这实际上更耗时,但代码生成器很少会遇到将/e替换字符串扩展成表达式的问题。这是一个非常平凡的转换,但可能足以满足最普遍的例子。
使用此功能,将任何错误的 preg_replace 调用编辑为 preg_replace_eval_replacement 并运行它 一次。这将打印出相应的 preg_replace_callback 块以替换它。
/**
 * Use once to generate a crude preg_replace_callback() substitution. Might often
 * require additional changes in the `return …;` expression. You'll also have to
 * refit the variable names for input/output obviously.
 *
 * >>>  preg_replace_eval_replacement("/\w+/", 'strtopupper("$1")', $ignored);
 */
function preg_replace_eval_replacement($pattern, $replacement, $subjectvar="IGNORED") {
    $pattern = preg_replace('/(\W[a-df-z]*)e([a-df-z]*)$/i', '$1$2', $pattern);
    $replacement = preg_replace_callback('/[\'\"]?(?<!\\\\)(?:[$]|\\\\)(\d+)[\'\"]?/', function ($m) { return "\$m[{$m[1]}]"; }, $replacement);
    $ve = "var_export";
    $bt = debug_backtrace(0, 1)[0];
    print "<pre><code>
    #----------------------------------------------------
    # replace preg_*() call in '$bt[file]' line $bt[line] with:
    #----------------------------------------------------
    \$OUTPUT_VAR = preg_replace_callback(
        {$ve($pattern, TRUE)},
        function (\$m) {
            return {$replacement};
        },
        \$YOUR_INPUT_VARIABLE_GOES_HERE
    )
    #----------------------------------------------------
    </code></pre>\n";
}

请注意,简单地复制粘贴并不是编程。您需要将生成的代码调整回实际的输入/输出变量名称或使用上下文。
具体来说,如果前面的 preg_replace 调用在 if 语句中使用,则 $OUTPUT = 赋值将不起作用。最好保留临时变量或多行代码块结构。
替换表达式可能需要更多的可读性改进或重新设计。
例如,stripslashes() 在文字表达式中经常变得多余。变量范围查找需要在回调中使用 use 或 global 引用。
引号不对称的 "-$1-$2" 捕获引用将通过纯转换为 "-$m[1]-$m[2] 语法上出现问题。
代码输出仅仅是一个起点。是的,这个工具如果能在线使用会更有用。这种代码重写方法(编辑、运行、编辑、编辑)有些不切实际。但对于那些习惯于任务为中心编码(更多步骤,更多发现)的人来说可能更易接近。因此,这种替代方案可能会减少一些重复的问题。

0

不应该使用标志 e(或一般的eval)。

您还可以使用T-Regx库

pattern('(^|_)([a-z])')->replace($word)->by()->group(2)->callback('strtoupper');

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接