前言
最典型的症状是:代码在本机跑得好好的,部署到新服务器或者把 PHP 升到 8.0 之后,页面直接白屏,日志里躺着这么几行:
PHP Fatal error: Uncaught Error: Call to undefined function curl_init() in /app/Http.php:42 PHP Fatal error: Uncaught Error: Class "Redis" not found in /app/Cache.php:17 PHP Warning: PHP Startup: Unable to load dynamic library 'gd' in Unknown on line 0这三行长得像,病因完全不同。第一行是扩展压根没装;第二行是 PECL 扩展没装或没加载;第三行是扩展文件存在但加载失败(路径不对、ABI 不匹配、依赖的库缺失)。
PHP 的扩展(extension)是独立的动态库文件(Linux 下是.so,Windows 下是php_xxx.dll),编译 PHP 时只有一部分会静态编进去,其余的要靠php.ini里的extension=指令在启动时动态加载。所以"扩展缺失"从来不是一个问题,而是三个问题:没装、没启用、启用失败。
本文按"先定位、再安装、最后排查为什么装完还不生效"的顺序讲,覆盖 Debian/Ubuntu、RHEL/CentOS、Docker 官方镜像、PECL、源码编译五种场景。下文所有命令以 PHP 8.0 为例。
一、先判断缺的是哪个扩展
报错信息里几乎都带着扩展名字,只是藏在函数名或类名里。把函数名去掉前缀,基本就是扩展名。
| 报错片段 | 缺失的扩展 | 说明 |
|---|---|---|
Call to undefined function curl_init() | curl | HTTP 客户端,未被allow_url_fopen替代 |
Call to undefined function mb_strlen() | mbstring | 多字节字符串必须装,非默认 |
Call to undefined function imagecreatefromjpeg() | gd | 图像处理,还可能缺 libjpeg/libpng |
Class "Redis" not found | redis | PECL 扩展 phpredis,不在 PHP 源码树里 |
Call to undefined function bcadd() | bcmath | 高精度计算 |
Call to undefined function simplexml_load_string() | simplexml | XML 解析 |
Call to undefined function intl_get_error_code() | intl | 国际化,依赖 ICU 库 |
Call to undefined function sodium_crypto_box() | sodium | PHP 7.2 起随源码分发,但仍需启用 |
could not find driver | pdo_mysql | 注意不是mysql,mysql扩展早已移除 |
Call to undefined function pcntl_fork() | pcntl | CLI 专用,FPM 下装了也用不了 |
定位清楚了再用命令确认。关键是必须在报错的那个 SAPI 下确认——Web 请求走的是 FPM,命令行走的是 CLI,两者读的php.ini经常不是同一个文件。
php -v php --ini php -m php -i | grep -i extension_dirphp --ini输出里的Loaded Configuration File就是实际生效的 ini 路径,Scan for additional .ini files in是附加目录(Debian 系在/etc/php/8.0/fpm/conf.d/,CentOS 系在/etc/php.d/)。php -m列出所有已加载的模块。
写一个探测器脚本更直观,把它放在能被 Web 访问到的地方(用完立刻删):
<?php // probe.php —— PHP 8.0+ 可直接运行,CLI 与 FPM 通用 declare(strict_types=1); header('Content-Type: text/plain; charset=utf-8'); $required = [ 'curl' => 'HTTP 请求(curl_init)', 'mbstring' => '多字节字符串(mb_strlen)', 'gd' => '图像处理(imagecreatefromjpeg)', 'openssl' => '加密与 TLS(openssl_encrypt)', 'pdo_mysql' => 'MySQL PDO 驱动', 'redis' => 'phpredis 客户端(new Redis)', ]; echo 'SAPI : ', PHP_SAPI, PHP_EOL; echo 'PHP : ', PHP_VERSION, PHP_EOL; echo 'ini : ', php_ini_loaded_file() ?: '(none)', PHP_EOL; echo 'ext_dir : ', ini_get('extension_dir'), PHP_EOL; echo str_repeat('-', 52), PHP_EOL; $missing = []; foreach ($required as $ext => $desc) { $loaded = extension_loaded($ext); printf("%-10s %-5s %s\n", $ext, $loaded ? 'OK' : 'MISS', $desc); if (!$loaded) { $missing[] = $ext; } } if ($missing !== []) { echo str_repeat('-', 52), PHP_EOL; echo '缺失: ', implode(', ', $missing), PHP_EOL; }在 Web 下访问一次,再在命令行跑一次php probe.php,两份输出对比。如果命令行显示curl OK而 Web 显示curl MISS,那就是 FPM 的 ini 目录里没有对应的.ini文件,扩展本身已经装好了,去conf.d目录补一个软链或新增一行即可。
二、按运行环境选安装方式
同一个扩展,在不同环境下装法完全不同。用错命令的典型后果是"装是装了,装给了另一个 PHP"。
| 环境 | 安装命令 | 备注 |
|---|---|---|
| Debian/Ubuntu + ondrej PPA | apt-get install php8.0-curl | 包名必须带8.0前缀 |
| RHEL/CentOS + Remi 仓库 | dnf install php80-php-curl | 还有独立的php80-php-fpm |
| Docker 官方镜像 | docker-php-ext-install curl | 只对源码树内置扩展有效 |
| PECL 扩展(redis/xdebug) | pecl install redis | 装完还要手动写 ini |
| 任意环境源码编译 | phpize && ./configure && make install | 需要 php-dev/php-devel |
Debian/Ubuntu 下,ondrej/phpPPA 提供的包名一律是php8.0-xxx:
sudo apt-get update sudo apt-get install -y php8.0-curl php8.0-mbstring php8.0-gd php8.0-xml php8.0-bcmath php8.0 -m | grep -E 'curl|mbstring|gd'RHEL/CentOS 用 Remi 的软件集合,注意扩展包是php80-php-前缀,和php80-主包分开管理:
# 先按 Remi 仓库官方说明为当前发行版安装 remi-release 包,再执行下面几步 sudo dnf module reset php sudo dnf module enable php:remi-8.0 sudo dnf install -y php80-php-fpm php80-php-curl php80-php-mbstring php80-php-gdDocker 官方php:8.0-fpm镜像自带docker-php-ext-install和docker-php-ext-enable两个脚本。前者只负责 PHP 源码树里自带的扩展,PECL 上的扩展要用pecl装完再docker-php-ext-enable启用:
FROM php:8.0-fpm RUN apt-get update && apt-get install -y --no-install-recommends \ libpng-dev libjpeg62-turbo-dev libzip-dev libonig-dev \ && docker-php-ext-configure gd --with-jpeg \ && docker-php-ext-install -j"$(nproc)" gd zip bcmath pdo_mysql \ && pecl install redis \ && docker-php-ext-enable redis \ && rm -rf /var/lib/apt/lists/*phpize是源码编译的入口,它会为当前 PHP 生成构建脚手架。要注意phpize属于哪个 PHP,就必须用哪个 PHP 的php-config编译,否则 ABI 版本对不上:
cd /usr/src/php-src/ext/zip phpize ./configure --with-php-config=/usr/bin/php-config8.0 make -j"$(nproc)" sudo make installmake install会把zip.so放进extension_dir,并在最后打印一行提示,告诉你该往 ini 里加什么。
三、装完仍不生效的排查顺序
按下面的顺序逐层排除,基本三步内能定位。
第一步,确认 ini 被读取。用-d参数临时指定扩展,绕过 ini 文件验证扩展文件本身是否可用:
php -d extension=curl -r 'var_dump(extension_loaded("curl"));'如果这样能输出bool(true),但正常运行时不行,问题 100% 在 ini 配置,不在扩展。
第二步,看启动警告。php -m的输出前面如果混着PHP Warning: PHP Startup:,说明扩展文件被找到了但加载失败。常见原因是extension_dir指向的目录里根本没有那个文件:
php -i | grep -i '^extension_dir' ls -l "$(php -r 'echo ini_get("extension_dir");')"第三步,确认 Web 侧生效。CLI 通过不代表 FPM 通过。改完 ini 后必须让 FPM 重新读取:
sudo systemctl restart php8.0-fpm sudo systemctl status php8.0-fpm --no-pager最后补一个优雅降级的写法。如果扩展在某些环境(比如本地开发机)确实装不上,用function_exists()做能力探测,把错误推迟到真正调用时再抛,比白屏友好得多:
<?php // http-client.php —— PHP 8.0+ declare(strict_types=1); function http_get(string $url, int $timeout = 5): string { if (function_exists('curl_init')) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_FOLLOWLOCATION => true, ]); $body = curl_exec($ch); if ($body === false) { $err = curl_error($ch); curl_close($ch); throw new RuntimeException("curl 请求失败: {$err}"); } curl_close($ch); return $body; } if (filter_var(ini_get('allow_url_fopen'), FILTER_VALIDATE_BOOL)) { $body = @file_get_contents($url); if ($body === false) { throw new RuntimeException('stream 请求失败'); } return $body; } throw new RuntimeException('curl 扩展缺失且 allow_url_fopen 已关闭,无法发起 HTTP 请求'); } echo http_get('http://127.0.0.1:8000/health'), PHP_EOL;常见坑点
❌ 在有 ondrej PPA 的机器上执行apt-get install php-curl✅ 必须带版本号:apt-get install php8.0-curl,装完用apt-cache policy php8.0-curl确认候选版本
❌ 把opcache当成普通扩展写extension=opcache✅ opcache 和 xdebug 是 Zend 扩展,必须写成zend_extension=opcache,否则要么报Unable to load dynamic library,要么静默不生效
❌ 在 Linux 上照抄 Windows 教程写extension=php_curl.dll✅ Linux 写extension=curl或extension=curl.so(PHP 会自动补.so后缀)
❌ 只在 CLI 下用php -m验证通过就上线 ✅ 用探测器脚本在 Web 下再验一次,因为 CLI 和 FPM 读的 ini 常常是两个文件
❌ 遇到json相关报错就去找php8.0-json这个包 ✅ PHP 8.0 起 JSON 是核心组件且不可禁用,extension_loaded('json')恒为true,报错一定在别的地方
❌ 手动把.so拷进extension_dir就认为装完了 ✅ 拷贝只是第一步,还要在conf.d目录里新增<名字>.ini并写入extension=<名字>
❌ 同时装 igbinary 和 redis 时,ini 里把extension=redis写在前面 ✅ 顺序必须是extension=igbinary在前、extension=redis在后,否则 redis 能用但 igbinary 序列化器不会被启用
❌ 把pcntl、posix装到 FPM 里去跑多进程任务 ✅ 这两个扩展只在 CLI SAPI 下可用,Web 环境必须用消息队列或外部进程管理器
总结
| 阶段 | 关键动作 | 判断依据 |
|---|---|---|
| 定位 | 从报错函数名反查扩展名,用探测脚本在正确 SAPI 下确认 | extension_loaded()返回 false |
| 安装 | 按发行版选php8.0-xxx/php80-php-xxx/docker-php-ext-install/pecl | 包管理器返回成功 |
| 启用 | 在对应 ini 目录新增extension=行,重启 FPM | php -m里出现模块名 |
| 排查 | -d临时加载、看 Startup 警告、查extension_dir | -d能加载说明配置问题 |
扩展缺失问题看着零散,其实只有一条主线:扩展文件、ini 配置、SAPI 三者必须同时对得上。任何一环错位都会表现成同一句Call to undefined function,而真正的病因要靠php --ini和探测脚本去分辨,而不是靠反复重装。