返回

告别 create_function:修复 PHP 7.2+ undefined function 错误

php

告别 create_function():修复 PHP 7.2+ 中的 undefined function 错误

写 PHP 代码的时候,你可能搬运过一些老代码,或者是在升级旧项目时,突然遇到了一个报错: Uncaught Error: Call to undefined function create_function()。接着你检查代码,发现罪魁祸首是类似下面这样的一段:

$callbacks[$delimiter] = create_function(
  '$matches',
   "return '$delimiter' . strtolower(\$matches[1]);"
);

这段代码想用 create_function() 创建一个简单的回调函数。 但在新版本的 PHP 里,这招不灵了,直接就报错。 别慌,这个问题很常见,也好解决。咱们这就来看看为什么会这样,以及怎么把代码改对。

1. 问题根源:为何 create_function() 被废弃?

简单说,create_function() 这个函数已经过时了。

  • PHP 7.2 开始: 它被标记为 “已废弃” (deprecated) 。这意味着 PHP 开发团队不推荐再使用它了,并且计划在未来版本中移除。这时候用它,一般会收到一个 E_DEPRECATED 级别的提醒,程序通常还能跑,但这是个警告信号。
  • PHP 8.0 开始: 它被 彻底移除 (removed) 。所以,一旦你的环境升级到 PHP 8.0 或更高版本,任何调用 create_function() 的地方都会直接触发 Fatal Error,就像开头提到的 undefined function 错误,程序会中断执行。

那 PHP 为啥要跟 create_function() 过不去呢? 主要有几个原因:

  1. 安全风险: create_function() 本质上是 eval() 的一个封装。它接收字符串形式的代码,然后在运行时动态解析执行。如果传递给它的字符串(尤其是包含参数列表或函数体的部分)可能受到外部输入(比如用户请求)的影响,就存在代码注入 的风险。攻击者可能构造恶意字符串,让服务器执行任意 PHP 代码。这可是个大漏洞!
  2. 性能问题: 每次调用 create_function(),PHP 引擎都需要在运行时解析函数参数和函数体字符串,然后编译成可执行的 opcode。这个过程相比起原生的 PHP 函数定义或者现代的匿名函数,开销要大得多,性能自然就差一些。在高并发或者频繁调用的场景下,这种性能差异会更明显。
  3. 可读性和维护性差: 把代码写成字符串,简直是维护者的噩梦。
    • 代码高亮基本失效,IDE 很难正确识别和着色字符串里的代码。
    • 自动补全、语法检查、代码跳转等 IDE 辅助功能几乎完全用不上。
    • 调试困难,在字符串代码里打断点或者追踪变量简直是折磨。
    • 静态分析工具(如 PHPStan、Psalm)也难以分析字符串里的代码逻辑,很多潜在问题发现不了。

基于以上这些缺点,PHP 社区决定用更安全、更高效、更易维护的方式来替代它。

2. 解决方案:拥抱现代 PHP 语法

替代 create_function() 的最佳方案是使用 PHP 内建的 匿名函数 (Anonymous Functions) ,也叫 闭包 (Closures) 。如果你的 PHP 版本够新 (7.4+),还可以用更简洁的 箭头函数 (Arrow Functions)

下面我们分别看看怎么用这两种方式改造开头的示例代码。

2.1 方案一:使用匿名函数 (Closures)

匿名函数允许你定义一个没有指定名称的函数,非常适合用作回调。

原理和作用:

匿名函数是正经的 PHP 函数结构,只不过没有名字。它可以像普通变量一样被赋值给变量、作为参数传递。当匿名函数需要访问其定义时所在作用域的变量时(比如例子中的 $delimiter),就需要使用 use ,将这些变量“捕获”到函数内部,这就是所谓的“闭包”。

这样做的好处是:

  • 安全性高: 代码不再是字符串,是原生的 PHP 语法,彻底杜绝了 eval() 带来的代码注入风险。
  • 性能更好: PHP 引擎可以直接解析和优化匿名函数,通常比 create_function() 效率更高。
  • 可读性强: 代码就是代码,享受 IDE 的各种支持(高亮、检查、补全),维护起来舒服多了。

改造代码:

将原来的 create_function() 调用替换成如下的匿名函数:

// 假设 $delimiter 变量在当前作用域已定义,例如:
$delimiter = '_'; // 示例值

// 使用匿名函数替代 create_function
$callbacks[$delimiter] = function ($matches) use ($delimiter) {
    // 函数体和原来 create_function 字符串里的逻辑一样
    // 注意:这里 $matches[1] 不需要反斜杠转义了
    return $delimiter . strtolower($matches[1]);
};

// 后面可以这样使用 (例如配合 preg_replace_callback)
// $output = preg_replace_callback('/some_pattern(.)/', $callbacks[$delimiter], $input);

代码解释:

  • function ($matches) 定义了一个匿名函数,接收一个参数 $matches
  • use ($delimiter) 是关键,它告诉这个匿名函数:“你需要用到外部作用域定义的 $delimiter 变量,把它引进来”。这样函数内部才能访问 $delimiter。如果不使用 use,函数内部直接访问 $delimiter 会报错(undefined variable)。
  • 函数体 { return $delimiter . strtolower($matches[1]); } 就是原来的逻辑,注意因为是原生 PHP 代码, $matches[1] 前面不需要像 create_function 里那样加反斜杠 \ 转义 $ 符号了。

安全建议:

  • 匿名函数本身就比 create_function() 安全得多。主要的安全考量转移到了如何处理 $matches 数据,确保它符合预期。通常 $matches 来自于可信的操作(如 preg_replace_callback),风险较低。但如果数据源不可控,仍需谨慎处理 strtolower() 等函数可能遇到的非预期输入(虽然这里主要是大小写转换,风险不大)。

进阶使用技巧:

  • 类型提示: 为了代码更健壮、更清晰,可以给参数和返回值加上类型提示(PHP 7.0+):

    $callbacks[$delimiter] = function (array $matches) use ($delimiter): string {
        // 明确 $matches 是数组,返回值是字符串
        // 这里假设 $matches[1] 总是存在且是字符串,
        // 实际应用中可能需要增加 isset() 或 is_string() 检查
        if (isset($matches[1]) && is_scalar($matches[1])) {
             return $delimiter . strtolower((string)$matches[1]);
        }
        // 根据业务逻辑返回默认值或抛出异常
        return ''; // 或者 throw new InvalidArgumentException('Invalid matches data');
    };
    
  • 引用捕获: 如果希望匿名函数能修改外部作用域的变量(不推荐,但技术上可行),可以在 use 时加上 &use (&$delimiter)。不过,通常回调函数只需要读取外部变量,用值捕获(不加 &)更常见也更安全。

2.2 方案二:利用箭头函数 (Arrow Functions) (PHP 7.4+)

如果你的项目环境是 PHP 7.4 或更高版本,那么还有一种更简洁的选择:箭头函数。

原理和作用:

箭头函数是 PHP 7.4 引入的,提供了一种更精简的匿名函数写法,特别适合用于简单的表达式型回调。它的核心特点:

  • 简洁语法: 使用 fn 关键字,省略了 functionusereturn 关键字,以及花括号 {}
  • 自动捕获作用域变量: 箭头函数自动 按值捕获(by-value)其定义时所在作用域的变量,无需手动写 use。这是它相比普通匿名函数最大的便利之处。
  • 单一表达式: 箭头函数体只能包含一个表达式,该表达式的结果就是函数的返回值。

改造代码:

使用箭头函数,代码可以变得非常短:

// 同样假设 $delimiter 变量在当前作用域已定义
$delimiter = '_'; // 示例值

// 使用箭头函数替代 (需要 PHP 7.4+)
$callbacks[$delimiter] = fn($matches) => $delimiter . strtolower($matches[1]);

// 使用方式同上
// $output = preg_replace_callback('/some_pattern(.)/', $callbacks[$delimiter], $input);

代码解释:

  • fn($matches) 定义了一个箭头函数,接收 $matches 参数。
  • => 后面紧跟着函数体表达式 $delimiter . strtolower($matches[1])
  • 它自动捕获了外部的 $delimiter 变量,无需 use 声明。
  • 整个表达式的值就是函数的返回值。

安全建议:

  • 同匿名函数一样,箭头函数本身没有 eval() 风险,是安全的。关注点仍在数据处理逻辑上。

进阶使用技巧:

  • 类型提示: 箭头函数同样支持参数和返回值的类型提示 (PHP 8.0+ 对返回值类型提示支持更好):

    // PHP 8.0+ 可以给箭头函数添加返回类型
    $callbacks[$delimiter] = fn(array $matches): string => $delimiter . strtolower($matches[1] ?? '');
    // 注意:为防止 $matches[1] 不存在产生 warning/error,可以加个 null 合并运算符 ??
    
  • 适用场景: 箭头函数非常适合那种只需要一行简单计算或转换的回调,比如数组函数 array_map, array_filter 或像本例中的 preg_replace_callback 的简单回调。如果函数逻辑比较复杂,包含多条语句、条件判断等,还是用普通的匿名函数更合适,可读性更好。

3. 如何选择?

  • 如果你的 PHP 环境 低于 7.4 ,或者回调函数的逻辑比较复杂(超过一个简单表达式),那么匿名函数 (Closure) 是你的不二之选。
  • 如果你的 PHP 环境 等于或高于 7.4 ,并且回调逻辑非常简单(就是一个表达式返回值),那么箭头函数 (Arrow Function) 能让代码更简洁、更现代。

无论选择哪种,它们都比古老的 create_function() 要好得多,不仅解决了迫在眉睫的 undefined function 错误,还提升了代码的安全性、性能和可维护性。赶紧动手把你代码库里的 create_function() 都换掉吧!