告别 create_function:修复 PHP 7.2+ undefined function 错误
2025-04-20 13:05:39
告别 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()
过不去呢? 主要有几个原因:
- 安全风险:
create_function()
本质上是eval()
的一个封装。它接收字符串形式的代码,然后在运行时动态解析执行。如果传递给它的字符串(尤其是包含参数列表或函数体的部分)可能受到外部输入(比如用户请求)的影响,就存在代码注入 的风险。攻击者可能构造恶意字符串,让服务器执行任意 PHP 代码。这可是个大漏洞! - 性能问题: 每次调用
create_function()
,PHP 引擎都需要在运行时解析函数参数和函数体字符串,然后编译成可执行的 opcode。这个过程相比起原生的 PHP 函数定义或者现代的匿名函数,开销要大得多,性能自然就差一些。在高并发或者频繁调用的场景下,这种性能差异会更明显。 - 可读性和维护性差: 把代码写成字符串,简直是维护者的噩梦。
- 代码高亮基本失效,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
关键字,省略了function
、use
和return
关键字,以及花括号{}
。 - 自动捕获作用域变量: 箭头函数自动 按值捕获(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()
都换掉吧!