症状:php.ini 里开着的,扩展却始终不加载
环境是 Windows Server 2012 R2 + XAMPP + PHP 8.3.33。php.ini 里 extension=curl 前面的分号早就去掉了,Apache 也重启过很多次,但 phpinfo() 里始终搜不到 curl 那一块,extension_loaded('curl') 恒为 false。
直接后果是:调用微信接口、百度推送接口这类 HTTPS 请求时,PHP 只能降级到 stream 通道。虽然也能用,但心里始终不踏实。
这个现象最让人困惑的地方在于:没有任何报错。php.ini 里明确开着,重启也做了,页面上不显示红色错误,日志里也找不到 PHP Startup 相关的行。它就像被静默跳过了一样。
动手之前,先确认三件事
这三步能拦掉绝大多数"改了没生效"的假象,务必先做完再看后面。
第一件:确认 php.ini 到底是哪一份
Windows 下经常存在多份 php.ini,改错了文件,后面全白做。不要相信自己的记忆,让它自己报:
C:\xampp\php\php.exe -r "echo php_ini_loaded_file();"
它输出的那一个才是要改的。也可以建一个临时文件 <?php phpinfo();,看页面里 Loaded Configuration File 那一行——看完记得删掉这个文件。
我这次就在这里绕了弯路:诊断脚本一度把 PHP 目录读成 C:\php,而那个路径根本不存在,真正的路径是 C:\xampp\php。
第二件:确认改完之后是真的重启了
XAMPP 控制面板上点 Restart 有时并不会重新读取 php.ini。可靠做法是 Stop,等按钮完全变红、进程真的退出,再 Start。
第三件:确认 DLL 文件本身存在
dir C:\xampp\php\ext\php_curl.dll
dir C:\xampp\php\libcrypto-3-x64.dll
dir C:\xampp\php\libssl-3-x64.dll
dir C:\xampp\php\libssh2.dll
四个都应该列出文件。缺任何一个,先补齐再往下走。
四个流传最广的解法,为什么都对不上症状
网上搜"PHP curl 加载失败",高票答案集中在下面这四条。它们本身都是对的,但只在特定前提下成立,套到本次症状上全部失效。
误诊一:SSL CA 根证书缺失
最常见的说法是:Windows 下的 PHP 不会读取系统根证书,访问 HTTPS 会报 SSL certificate problem: unable to get local issuer certificate,解法是下载 cacert.pem 并配置:
curl.cainfo = "C:\xampp\php\extras\ssl\cacert.pem"
这条有个隐藏前提:curl 扩展必须先加载成功。 扩展都没起来,curl.cainfo 这一行对它完全无效——配置项读进去了,但没有 curl 去消费它。
这是典型的因果倒置:CA 证书解决的是"curl 能用但访问 HTTPS 报错",而不是"curl 压根加载不了"。两者的报错表现也完全不同,前者会明确出现 SSL certificate problem 字样,后者是静默失败。
不过这一行后来还是加上了,因为扩展修好之后它就变成必需项了。详见第七节。
误诊二:TLS 版本过低
另一个说法是 Windows Server 2012 R2 默认只启用 TLS 1.0/1.1,而现代站点要求 TLS 1.2 以上,导致握手失败。
这条同样只在 curl 已加载的前提下才有意义。而且实际检查系统,TLS 1.2 早就启用了,这条直接排除。
误诊三:缺 VC++ 运行库
PHP 8.x 用较新的 VS 工具链编译,老系统常缺运行库。这条看起来很有希望,因为命令行里跑 php.exe 确实报过类似提示。
但有一个简单的反证:PHP 本身跑得好好的,phpinfo 页面能正常打开。php8ts.dll 同样依赖运行库,它要是真缺,PHP 根本起不来。
诊断脚本后来报 vcruntime140_1.dll 缺失,但回看真实依赖列表,里面只有 vcruntime140.dll(已存在),压根没有那个文件——属于脚本多查了一项造成的误报。系统里确实装了多个 VC++ Redistributable,这条可以排除。
误诊四:extension_dir 路径错误
extension_dir = "C:\xampp\php\ext"
路径正确,DLL 也在,排除。
真因:Apache 和 PHP 不在同一个目录
前面四条全部排除后,只剩一个方向:DLL 依赖链断裂。但文件明明都在,那断在哪?
答案在于 Windows 加载 DLL 时的搜索顺序:
| 顺序 | 搜索位置 |
|---|---|
| 1 | 加载者(php_curl.dll)自身所在目录,即 ext\ |
| 2 | 可执行文件所在目录,即 C:\xampp\apache\bin\ |
| 3 | System32 |
| 4 | PATH 环境变量 |
注意这个关键错位:
php_curl.dll → C:\xampp\php\ext\
libssl-3-x64.dll → C:\xampp\php\ ← 依赖在这
httpd.exe → C:\xampp\apache\bin\ ← 进程在这
php_curl.dll 被加载后要去找 libssl-3-x64.dll,它按上面的顺序找了一圈:ext\ 没有、apache\bin\ 没有、System32 没有、PATH 也没有。而那个文件其实就在 C:\xampp\php\——这个目录根本不在搜索列表里。
于是加载失败。而 PHP 对扩展加载失败的处理是静默跳过,既不在页面报错,也不写日志,所以看起来就是"配置开着却没生效"。
这也解释了为什么 XAMPP 官方包通常会在 apache\bin 里也放一份这些 DLL。
一条命令定性:CURL OK 还是 CURL FAIL
这是整个排查过程里最有价值的一步——它把"猜测"变成了"判定"。
cd /d C:\xampp\php
php.exe -n -d extension_dir="C:/xampp/php/ext" -d extension=curl -r "echo extension_loaded('curl')?'CURL OK':'CURL FAIL';"
这条命令绕开了 Apache,直接让 php.exe 加载 curl。而 php.exe 就在 C:\xampp\php——正是那些依赖 DLL 所在目录。如果它能加载成功,说明 DLL 本身全部完好。
两种结果对应完全不同的处理方向:
| 结果 | 说明 | 下一步 |
|---|---|---|
| CURL OK | DLL 全都是好的,纯粹是 Apache 找不到它们 | 复制到 apache\bin,见第六节 |
| CURL FAIL | 确实缺文件,命令行会直接打印出错的模块名 | 按报错补 DLL |
本次实测输出 CURL OK,假设成立。至此从"猜问题"变成了"确定问题"。
三种修复方案
方案一:把 DLL 复制到 Apache 目录(最简单)
copy C:\xampp\php\libcrypto-3-x64.dll C:\xampp\apache\bin\
copy C:\xampp\php\libssl-3-x64.dll C:\xampp\apache\bin\
copy C:\xampp\php\libssh2.dll C:\xampp\apache\bin\
copy C:\xampp\php\brotlidec.dll C:\xampp\apache\bin\
copy C:\xampp\php\nghttp2.dll C:\xampp\apache\bin\
后两个是 php_curl.dll 的可选依赖(brotli 压缩、HTTP/2 支持),存在就一起复制,不存在可跳过。
php8ts.dll 不需要复制——Apache 加载 PHP 模块时已经从 C:\xampp\php 载入过了。
方案二:用 LoadFile 让 Apache 预加载(不改目录结构)
打开 C:\xampp\apache\conf\httpd.conf,在 LoadModule 那一堆附近加:
LoadFile "C:/xampp/php/libcrypto-3-x64.dll"
LoadFile "C:/xampp/php/libssl-3-x64.dll"
LoadFile "C:/xampp/php/libssh2.dll"
注意这里用的是正斜杠。适合不想往 Apache 目录塞文件的场景。
方案三:把 PHP 目录加进 PATH(影响面最大)
系统环境变量里加入 C:\xampp\php,重启服务器生效。这会让所有程序都能找到这些 DLL,副作用范围也最大,一般不推荐。
三种方案任选其一即可,不要同时用。本次用的是方案一。
加载成功后,还要补两行配置
扩展加载成功只是第一步。要让它正常访问 HTTPS,CA 证书那一行现在才真正起作用:
curl.cainfo = "C:\xampp\php\extras\ssl\cacert.pem"
openssl.cafile = "C:\xampp\php\extras\ssl\cacert.pem"
两行都要:
| 配置项 | 作用于 |
|---|---|
curl.cainfo |
curl 扩展 |
openssl.cafile |
stream / openssl 通道(file_get_contents 走 https 时用) |
证书包从 https://curl.se/ca/cacert.pem 下载,放到 C:\xampp\php\extras\ssl\(没有 ssl 目录就新建)。
改完必须彻底重启 Apache,否则读到的还是旧配置。
如果命令行也返回 CURL FAIL
那就说明确实缺文件。命令行会直接打印出错的模块名,按这个名字搜:
dir /s C:\xampp\<缺失的模块名>.dll
搜到之后复制到 C:\xampp\php\ 和 C:\xampp\apache\bin\,再重启验证。
若提示的是运行库相关(如 vcruntime140_1.dll),去微软官网装 Visual C++ Redistributable for Visual Studio 2015–2022 x64,装完重启服务器(运行库要进系统,只重启 Apache 有时不够)。
实在修不好:用 stream 通道绕开
$ctx = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json; charset=utf-8\r\n",
'content' => json_encode($data, JSON_UNESCAPED_UNICODE),
'timeout' => 15,
'ignore_errors' => true,
],
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$res = file_get_contents($url, false, $ctx);
更稳妥的做法是让 WordPress 自己选:用 wp_remote_post(),它在 curl 不可用时会自动降级到 WP_Http_Streams。再补一层兜底——wp_remote_post 失败时改用 file_get_contents——基本就不会出问题了。
顺带说一句:如果你的服务器 curl 也是坏的,WordPress 后台的插件更新、主题更新、在线安装都会受影响,它们同样依赖 HTTP 层。修好 curl 之后这些也会跟着恢复正常。
避坑清单
| 坑 | 说明 |
|---|---|
| 改了 php.ini 却没生效 | 多半改的不是 php_ini_loaded_file() 报出来的那份 |
| 点了 Restart 以为重启了 | 必须 Stop 等变红再 Start,Restart 有时不重读配置 |
| curl 没加载就配 curl.cainfo | 顺序反了,此时这一行完全无效 |
| 看到 TLS 版本就往 TLS 上想 | TLS 只在 curl 已加载后才相关 |
| phpinfo 的 Build System 造成误导 | 显示 Server 2022 而系统是 2012 R2,看着像不兼容,实际只是编译环境,不代表跑不了 |
| 依赖扫描报一堆"缺失" | 先确认脚本读的 PHP 目录对不对,路径错了就会全判缺失 |
| 复制 DLL 后没重启 | Apache 已加载的进程不会重新找 DLL,必须彻底重启 |
| 诊断脚本用完没删 | 会暴露服务器绝对路径,务必删掉 |
排查顺序总结
下次再遇到类似问题,按这个顺序走,能少走很多弯路:
- 先确认扩展到底加载了没有。 这一条决定了后面所有配置有没有意义。
- 再确认 php.ini 是哪一份、有没有真重启。
- 然后用 CLI 那条命令定性——OK 说明 DLL 完好、是路径问题;FAIL 说明真缺文件。
- 最后再谈 CA 证书和 TLS。 它们是"能用但报错"层面的问题,不是"加载不了"层面的问题。
整个排查里最有用的一步,是那条绕开 Apache 直接测试的命令。它把一堆可能性收敛成了两个明确分支,避免了在 CA 证书、TLS、运行库这些方向上反复空转。
验证
C:\xampp\php\php.exe -m | findstr /i curl
输出 curl 即成功。也可以刷新 phpinfo 页面,此时应该能看到完整的 curl 信息块,以及 curl.cainfo 显示为你配置的路径。