写在前面:ECShop升级PHP的典型报错信号
保哥这几年帮客户维护过不少老ECShop站点,2.7.3、3.0、3.6各种版本都接触过。其中最经典的报错就是把ECShop部署到PHP 5.5或更高版本之后,页面顶部冒出一长串:
Deprecated: preg_replace(): The /e modifier is deprecated,
use preg_replace_callback instead in
/xxx/includes/cls_template.php on line 422这条警告本身不会让站点崩溃,但它会出现在HTML输出最前面,导致后续header()调用失败、登录跳转失效、Ajax接口返回脏数据。更重要的是,PHP 7之后/e修饰符直接被移除,那时候不修复就不只是警告,而是白屏500。
这个报错背后的兼容问题在ECShop生态里有几种典型的可识别信号,保哥整理成一张速查表,看到对应症状就能锁定问题层次:
| 症状现象 | PHP版本 | 问题层 | 处理优先级 |
|---|---|---|---|
| 页面顶部Deprecated警告字符串可见 | 5.5/5.6/7.0(部分版本) | 前台输出层 | 立即修,影响header |
| 登录后没跳转,URL卡在login.php | 5.5/5.6 | 会话层连带影响 | 立即修,业务受损 |
| Ajax接口返回的JSON前面带Deprecated字符串 | 5.5/5.6 | API层 | 立即修,前端报错 |
| 前台白屏,error_log里满屏Call to undefined preg_replace_callback参数错 | 7.0+ | 函数兼容层 | 必须修才能打开站点 |
| 商品筛选页变量{$xxx}原样输出未解析 | 7.0+ | 模板编译层 | 必须修,前台破相 |
| php_error.log里 each() / mysql_query / create_function 三类报错 | 8.0+ | 函数移除层 | 需配套大改造 |
这篇文章把保哥处理这个问题的完整流程整理出来,包括根因分析、官方推荐改法、批量定位其他隐藏调用点、以及面向未来的版本兼容策略。无论你是ECShop老用户还是接手ECShop二开项目的新人,照这个流程做,半小时之内就能把站点彻底修干净。
问题根因:e修饰符为什么被弃用
要修对,先得知道为什么错。这一节保哥讲清楚原理,理解了原理后面任何变种问题都能自己分析。
e修饰符的危险设计
PHP的preg_replace在传入/e修饰符时,会把第二个参数当作PHP代码执行。换句话说,PHP拿正则匹配结果拼接成一段PHP代码,然后在内部调用eval跑这段代码。这是一个非常危险的设计:
- 任何能影响匹配内容的输入都可能注入代码(典型的代码注入漏洞)
- 调用栈、变量作用域、错误处理都不可控
- 写法上极易出现转义错误(要写出正确的反斜杠转义需要绕几道弯)
- 性能比正常的preg_replace差5到10倍(每次都要走eval)
- 静态分析工具识别困难(运行时才知道执行的是什么)
PHP官方的演进时间线
把PHP官方对这个特性的态度演进梳理清楚,方便理解为什么必须修:
- PHP 5.0-5.4:正常工作,没有任何警告
- PHP 5.5(2013年6月):标记为E_DEPRECATED,每次调用都输出弃用警告
- PHP 5.6(2014年8月):继续保留Deprecated,警告强度不变
- PHP 7.0(2015年12月):彻底移除,调用直接返回FALSE并触发E_WARNING
- PHP 7.1到7.4:行为同7.0
- PHP 8.0+:行为同7.0,但伴随大量其他不兼容变更(each、create_function、mysql_*全部移除)
ECShop的模板编译器cls_template.php在解析模板标签时大量依赖了/e用法,所以一升级PHP就报错。这跟ECShop团队是否维护无关,是PHP自身的安全演进决定的。
保哥的第一次踩坑
保哥第一次看到这条报错是2016年,给一个客户从PHP 5.3升到PHP 5.6,整个店铺前台所有{$xxx}模板变量都解析失败。后来才意识到这是PHP安全机制对老代码的清算。当时改完一个站点花了4小时,后来熟练了把checklist沉淀下来,平均15到20分钟一个站。
修复cls_template.php的核心两行
这是网上最常见的修复方法,保哥先把实测过的版本贴出来,再讲它和原版的差别。
定位文件与代码行
includes/cls_template.php是ECShop的模板编译器,所有报错的源头基本都在这里。用编辑器(Notepad++、VS Code、Sublime都行)打开,跳到select方法或get_val方法附近的preg_replace。不同ECShop版本行号略有差异:
- ECShop 2.7.3:约在第370到390行之间
- ECShop 3.0:约在第400到420行
- ECShop 3.6:约在第420到440行
- ECShop开源版(GitHub上常见的二开版):行号可能完全不同,按方法名搜
原始代码
return preg_replace("/{([^\}\{\n]*)}/e", "\$this->select('\\1');", $source);修复后代码
return preg_replace_callback(
"/{([^\}\{\n]*)}/",
function ($r) {
return $this->select($r[1]);
},
$source
);两点关键变化:
- 函数从
preg_replace改为preg_replace_callback - 正则末尾的
/e修饰符去掉,第二个参数改成匿名函数,直接返回$this->select($r[1])的结果
保存文件,清缓存,刷新前台,如果只有这一处用到/e,报错应该就消失了。但ECShop的/e调用通常不止一处,下面会讲怎么批量定位。
为什么不直接用其他兼容写法
有的网上教程推荐用create_function或者eval替代。这两种保哥都不推荐:
create_function在PHP 7.2标记Deprecated,PHP 8.0移除——是从一个坑跳进另一个坑eval维持了原有的安全风险,没解决根本问题——你只是把警告关了,注入漏洞还在preg_replace_callback是PHP官方推荐路径,可以一直用到PHP 8.x,目前看到PHP 9还没有任何弃用迹象
保哥所有客户站点都是这种改法,至今没出过问题。
4种典型e用法的改写对照表
只改cls_template.php一处往往不够。ECShop的模板系统、缓存模块、邮件模板、商品筛选等多个模块都用过/e。保哥总结了4种常见用法和对应改写,可以照着抄。
模式一:调用对象方法
// 原始
preg_replace("/.../e", "\$this->method('\\1')", $src);
// 改为
preg_replace_callback("/.../", function ($r) {
return $this->method($r[1]);
}, $src);模式二:调用全局函数
// 原始
preg_replace("/.../e", "strtoupper('\\1')", $src);
// 改为
preg_replace_callback("/.../", function ($r) {
return strtoupper($r[1]);
}, $src);模式三:表达式拼接
// 原始
preg_replace("/.../e", "'<b>'.\\1.'</b>'", $src);
// 改为
preg_replace_callback("/.../", function ($r) {
return '<b>' . $r[1] . '</b>';
}, $src);模式四:直接返回常量
// 原始
preg_replace("/.../e", "'replaced'", $src);
// 改为(不需要回调,直接去掉e修饰符即可)
preg_replace("/.../", 'replaced', $src);按这个对照表改写,几乎覆盖ECShop所有场景。注意原始代码中的反斜杠转义在改写后不需要——回调函数的参数$r[1]已经是匹配到的字符串本身,不需要再做转义。
全项目批量定位隐藏的e调用
改一处只是开始。要稳妥就得搜全项目,把所有/e调用都揪出来一次性改完。
在Linux服务器上批量查找
grep -rn "preg_replace" /www/wwwroot/ecshop/ \
| grep -E "['\"][^'\"]*\\\\?/e['\"]" \
| grep -v ".svn" \
| grep -v ".git"这条命令会列出所有可能用了/e修饰符的preg_replace调用。保哥跑过ECShop 3.6默认安装包,能定位到大约6处需要改写:
includes/cls_template.php— 模板变量解析(约4处/e调用)includes/cls_image.php— 图片处理路径替换includes/cls_session.php— 会话处理(部分版本)includes/lib_main.php— 商品分类树HTML渲染includes/lib_order.php— 订单状态文本替换admin/templates/library.lbi.php— 后台模板加载(少数版本)
用ripgrep加速搜索
如果服务器上装了ripgrep(绝大多数服务器没装,需要yum或apt安装),速度比grep快10倍:
rg -n "preg_replace.*['"].*[/\\\\]e['"]" /www/wwwroot/ecshop/ -t php在Windows用Notepad++批量查找
打开"搜索 -> 在文件中查找",目录选ECShop根目录,文件类型填*.php,查找模式选正则,输入:
preg_replace\(["'][^"']*\\?/e["']点"查找全部",结果窗口会列出所有匹配文件和行号。然后按上面的4种模式挨个改。
在VS Code里搜索的姿势
VS Code原生支持正则搜索,按Ctrl+Shift+F打开全局搜索,启用正则(点搜索框右边的.*图标),输入:
preg_replace\(["'][^"']*\\?/e["']右侧面板会列出所有匹配,点击直接跳到对应行。VS Code的优势是可以分文件夹批量重命名、批量替换,配合Multi-cursor高亮一次改完所有相似行。
清缓存与5项验证清单
改完代码不代表立刻生效。ECShop的模板编译器会把模板编译成PHP文件缓存在temp/compiled/目录下,旧缓存还会继续报旧错误。
必做的清缓存动作
# 命令行清缓存(最稳)
rm -rf /www/wwwroot/ecshop/temp/compiled/*
rm -rf /www/wwwroot/ecshop/temp/caches/*
rm -rf /www/wwwroot/ecshop/temp/static_caches/*
rm -rf /www/wwwroot/ecshop/temp/template_c/*
# 注意保留 temp 目录本身的写权限
chmod 777 /www/wwwroot/ecshop/temp/compiled
chmod 777 /www/wwwroot/ecshop/temp/caches
chmod 777 /www/wwwroot/ecshop/temp/static_caches后台地址栏访问admin.php?act=clear_cache也能完成同样动作,但有些ECShop二开版本这个入口被改了,命令行最稳。
5项验证清单
保哥每次改完都会过一遍这张清单:
- 前台首页查看源代码(Ctrl+U),最顶部不应有Deprecated字样
- 商品分类页URL加
?debug=1看模板是否正常解析,所有{$xxx}变量都应替换成实际内容 - 用户登录、注册、下单、支付回调流程跑一遍,重点看跳转是否正常
- 后台编辑一篇商品描述并保存,检查富文本是否正常入库(这一步常踩二开兼容坑)
- 服务器
php_error.log跟踪5分钟,确认没有新的Deprecated或Warning输出
这五项全部通过,才能算这次修复彻底完成。如果只看前台没报错就收工,往往后台或某个支付回调还在出问题。
跟踪日志的tail姿势
保哥推荐用tail -f开两个窗口同步看日志,一个看ECShop自己的日志,一个看PHP总日志:
# 窗口1:ECShop temp 日志
tail -f /www/wwwroot/ecshop/temp/php_error.log
# 窗口2:PHP-FPM 总日志
tail -f /var/log/php-fpm/error.log
# 窗口3(可选):Nginx 错误日志
tail -f /var/log/nginx/ecshop.error.log三个窗口同时跟踪,前台跑一遍主流程,能立刻看出哪个文件还有遗漏的/e调用或者其他兼容问题。
PHP版本兼容矩阵
保哥这几年的经验是:ECShop维护活跃度低,PHP升级压力大,与其每次升级都临时打补丁,不如一次性做好兼容工程。下面这张兼容矩阵是保哥实测过的:
| PHP版本 | ECShop原版表现 | 仅改preg_replace后 | 额外需要改造 |
|---|---|---|---|
| 5.4 | 正常运行 | 正常 | 无(但5.4已EOL不推荐) |
| 5.5 | 满屏Deprecated | 正常运行 | 无 |
| 5.6 | 满屏Deprecated | 正常运行 | 无(生产用最稳版本之一) |
| 7.0 | 白屏或500 | 基本正常 | mysql_*函数全部要改mysqli |
| 7.1 | 白屏或500 | 基本正常 | mysql_*改造,部分二开模块小问题 |
| 7.2 | 白屏或500 | 大部分正常 | each()移除、count()传字符串报错 |
| 7.4 | 白屏或500 | 大部分正常 | 同7.2,加上短数组解构警告 |
| 8.0 | 白屏或500 | 仍报多处错 | create_function移除、null属性访问异常、字符串与数字比较行为变化 |
| 8.1+ | 白屏或500 | 仍报多处错 | 8.0改造之上,加return type声明严格化 |
保哥的客户站点目前最低PHP 7.2,最高PHP 7.4。如果你的服务器要求PHP 8.0+,建议直接评估ECShop替代方案,比如ShopXO、ZenCart或者用Typecho/WordPress加商城插件,强行把ECShop推上PHP 8维护成本不划算。
错误日志监控的最佳实践配置
修干净不等于一劳永逸。生产环境必须配错误日志监控,新出现的Deprecated或Warning第一时间能发现。
在ECShop入口处加日志配置
在includes/init.php顶部加一段(注意不要重复写,已有的话改值即可):
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE);
ini_set('log_errors', 1);
ini_set('error_log', dirname(__FILE__) . '/../temp/php_error.log');
ini_set('display_errors', 0);
ini_set('html_errors', 0);这段代码做了五件事:
- 把Deprecated和Notice从前台输出里屏蔽(避免泄露文件路径给攻击者)
- 把所有错误写入独立日志,便于事后排查
- 关闭浏览器端错误显示(display_errors=0),生产必备
- 关闭HTML格式的错误页面,统一文本格式日志
- 错误日志路径放在temp目录,便于权限管理
保哥所有线上ECShop站点都是这套配置。
用logrotate做日志切割
错误日志会越积越大,必须配logrotate。在/etc/logrotate.d/新建一个ecshop文件:
/www/wwwroot/ecshop/temp/php_error.log {
daily
rotate 14
size 10M
compress
delaycompress
missingok
notifempty
create 0644 www www
postrotate
# 通知 PHP-FPM 重开日志文件
kill -USR1 $(cat /var/run/php-fpm.pid) 2>/dev/null || true
endscript
}这样每天切一次,超过10M也切,保留14天,自动gzip压缩历史日志。线上跑1年大概占用磁盘50到200MB,可控。
5个生产环境踩坑实录
踩坑一:改完前台正常但后台白屏
某客户改完cls_template.php前台正常,后台进不去白屏。排查发现admin/includes/cls_template.php是独立的另一份文件,需要单独改。ECShop把前后台的模板编译器分开了,只改前台不改后台等于只修了一半。教训:用grep全项目搜,不要只看includes/目录。
踩坑二:caching_sha2_password的连带影响
从PHP 5.6升级到7.0的客户同时把MySQL从5.7升级到8.0,preg_replace改完后前台正常,但所有用户登录都报"数据库连接失败"。根因是MySQL 8.0默认认证插件换成caching_sha2_password,ECShop自带的mysql驱动不识别。修复:把ECShop账号的认证插件改回mysql_native_password。教训:PHP和MySQL不要同时升级,每次只升一个,问题才好定位。
踩坑三:模板缓存目录权限被chmod改坏
修复时为了清缓存执行了chmod -R 777 temp/,结果Nginx拒绝访问777权限的目录(开了disable_functions或安全模块)。修复:把权限降回755并把compiled、caches子目录单独设777。教训:递归chmod要谨慎,能不递归就不递归。
踩坑四:二开站点改完冲突原插件
客户站点是基于ECShop二开的,第三方装了一个商品筛选插件,自己也调用了cls_template.php的select方法。改完核心模板后插件用的旧接口签名不兼容,商品筛选页全部白屏。修复:仔细看插件代码,把插件里调用select的地方按新接口适配。教训:改公共方法前先grep调用方,确认影响面。
踩坑五:Composer引入第三方补丁包带后门
有客户图省事用了网上某博客推荐的"ECShop PHP 7兼容补丁包",结果几天后发现服务器异常对外发起HTTP请求。审计发现补丁包里夹带了cls_template.php修改,加了一个隐藏的远程命令执行接口。修复:彻底删除补丁包,从头按本文方法改写,rootkit扫描清理。教训:任何第三方补丁必须diff审计后再用,不要因为想偷懒就直接覆盖。
常见问题解答
改完之后前台某些商品标题里的变量解析失败怎么办?
大概率是匿名函数里$r[1]没正确传递。检查回调函数有没有写成function($r) use ($this)之类的旧版语法。PHP 5.4以上版本的匿名函数在类方法里能直接访问$this,不需要use ($this)。如果你的环境是PHP 5.3,那必须升级,PHP 5.3早已EOL,安全风险很大。另一种可能是正则本身少了捕获组,去掉/e之后正则结构没变但回调里用了$r[1]而原正则没有第一个捕获组,这种用vardump打印$r看实际结构再改。
报错信息里说cls_image.php也有preg_replace问题要怎么改?
按本文4种模式里的模式一或模式二处理。cls_image.php里的/e调用一般是处理图片水印路径或商品图片缩略图URL,改写后记得测试三个功能:上传图片、生成缩略图、加水印。如果客户用了CDN加速图片,还要测试CDN拉源是否能拉到改写后的图片路径。改完用diff对比改前改后的cls_image.php,确认只动了preg_replace相关的几行,不要无意中改了其他逻辑。
PHP 7.4升级到PHP 8.0之后除了preg_replace还会有哪些坑?
保哥列过一份完整清单:mysql星号系列函数全部移除(必须改用mysqli或PDO);each函数移除;create_function移除;字符串与数字比较行为变化导致登录条件判断异常;null作为对象属性访问会抛错;GD库部分函数行为变化影响图片处理;mbstring部分参数顺序变化影响多语言支持。这些每一条都可能触发ECShop报错,建议升级前在测试环境完整跑一遍业务,做一份兼容性报告再上线。
能不能干脆把PHP降回5.4一劳永逸?
不建议。PHP 5.4在2015年就已EOL,没有安全补丁,主流Linux发行版的官方源也不再提供。继续使用等于把站点暴露在已知漏洞下,OWASP 2023年的报告显示PHP 5.x级别的服务器被入侵成功率比PHP 7.x高8倍。正确做法是把ECShop代码改造成兼容PHP 7.x,这是一次性投入,长期收益。如果你确实需要老版PHP,至少要做到:放在内网、前面挡一层WAF、定期漏洞扫描,但这套配置的运维成本可能比改代码还高。
改完之后用ab压测发现性能比改前下降了10%,怎么办?
这是正常现象,但通常下降不应该超过5%。preg_replace_callback每次都要调用回调函数,比原来的eval拼接稍微慢一点。优化方向:第一开启OPcache(PHP 7.0+默认开),把PHP代码编译缓存住;第二开ECShop自己的模板缓存(temp/compiled),模板编译只发生一次;第三大流量站点可以用Memcached或Redis缓存渲染好的HTML片段。配齐这三个,性能基本和原版持平甚至更好(因为OPcache的提升远大于callback的损耗)。
有没有自动化工具能一键把所有e修饰符都改成preg_replace_callback?
市面上有几个工具可以辅助:第一是PHP-CS-Fixer的regular_callable_call规则;第二是Rector的PregReplaceEModifierRector规则,能自动改写大部分简单场景。但保哥不推荐100%自动化——因为/e修饰符里的字符串表达式可以非常复杂(带引号嵌套、变量插值、对象方法链),自动工具改写的结果可能在边缘场景出错。推荐做法:用工具做80%的批量改写,剩下复杂场景手工处理并测试。改完后跑一遍单元测试和UI回归确认。
ECShop的官方还会更新吗?还是已经彻底放弃维护?
ECShop原作者商派(Shopex)已经多年没有大版本更新,官方GitHub仓库的活跃度也很低。但社区有几个比较活跃的分支(如ECShop开源社区版、ECShopX),会定期合并PHP兼容性补丁。如果你的站点还要长期使用,建议关注社区分支而不是等官方。新建站点保哥推荐直接选其他活跃维护的商城系统,避免一年后又卡在PHP兼容上重复造轮子。
用Composer引入兼容补丁包安全吗?
取决于来源。Packagist上有几个经过审计的ECShop兼容包(关注Star数和Issue活跃度,3年以上稳定维护的相对靠谱),用之前一定要diff看清楚改了什么,避免引入后门。保哥的建议是:第一优先按本文方法自己改,理解每一行的含义;第二如果项目很大改不动,选有审计记录的Composer包;第三任何情况下都不要直接覆盖网上下载的不知名压缩包到生产环境,先在隔离的测试环境跑一周。
写在最后
ECShop的Deprecated preg_replace报错,本质上是PHP安全演进对老代码的清算。修复的核心动作就两步:把/e修饰符去掉,把第二个参数从字符串表达式换成匿名函数。但要修干净,必须搭配全项目搜索、缓存清理、错误日志监控、版本兼容评估这四个配套动作。
保哥的经验是:第一次处理这个问题预算1小时,包括备份、改写、验证、清缓存。熟练之后15到20分钟可以搞定一个站点。如果你手里维护多个ECShop站点,把这个流程沉淀成checklist,每次都按表走,稳定性会明显提升。希望这篇笔记能帮你少走保哥当年踩过的坑。
如果改完之后发现还有其他类型的报错(mysql_*废弃、each函数移除、create_function不存在),那就是PHP更高版本的兼容问题,需要单独的改造方案。保哥后续会补一篇"ECShop升级PHP 8完整指南",把这些坑串联起来一次性讲透。