PAYATHON 2026

Symfony 中如何禁用 OPcache 解决类名重复声明错误

排障赵师傅

结论

在 Debian、Nginx、PHP-FPM 环境中,OPcache 由 PHP 的 .ini 文件控制,Nginx、Laravel、Symfony、Artisan 和 Composer 都不会改变这一机制。Web 请求和 php artisan 使用不同的 PHP 配置:

  • Nginx 通常把请求交给 PHP-FPM,读取 fpm 配置。
  • php artisan route:list 由 PHP CLI 执行,读取 cli 配置。

如果错误只在 php artisan route:list 中出现,可以先通过一次性参数关闭 CLI OPcache:

php -d opcache.enable=0 -d opcache.enable_cli=0 artisan route:list

命令仍报出相同错误时,通常是同一个类被加载了两次。接下来应检查重复的类定义、Composer 自动加载配置、文件名大小写冲突和 PHP 7.4 的预加载配置。

长期关闭 OPcache 不适合作为最终修复方案。生产环境关闭 OPcache 会降低 PHP 性能,而类名重复声明通常源于代码或自动加载问题。

先确认实际使用的配置文件

配置路径应以当前环境为准,不要直接套用网上的示例。先分别确认 CLI 和 PHP-FPM 加载了哪些配置。

查看 CLI 配置:

php --ini
php -i | grep -E 'Loaded Configuration|Scan this dir|opcache.enable|opcache.enable_cli|opcache.preload'

检查 OPcache 是否已加载:

php -m | grep -i opcache

检查当前 CLI 进程中的状态:

php -r 'var_dump(ini_get("opcache.enable"), ini_get("opcache.enable_cli"), ini_get("opcache.preload"));'

常见的 Debian PHP 7.4 安装可能使用以下配置文件:

/etc/php/7.4/cli/php.ini
/etc/php/7.4/cli/conf.d/10-opcache.ini
/etc/php/7.4/fpm/php.ini
/etc/php/7.4/fpm/conf.d/10-opcache.ini
/etc/php/7.4/mods-available/opcache.ini

文件名和数字前缀会随安装来源变化,最终应以 php --ini 和 PHP-FPM 实际加载的配置为准。

只为 Artisan 临时禁用 OPcache

排查 route:list 时,可以先用命令行参数关闭 OPcache,无需修改系统配置:

php -d opcache.enable=0 -d opcache.enable_cli=0 artisan route:list

也可以运行下面的命令检查状态:

php -d opcache.enable=0 -d opcache.enable_cli=0 \
  -r 'var_dump(ini_get("opcache.enable"), ini_get("opcache.enable_cli"), opcache_get_status(false));'

相应配置通常会显示为 "0",opcache_get_status(false) 会返回 false。

PHP 7.4 的常见默认配置已经关闭 opcache.enable_cli。可以用下面的命令确认:

php -r 'var_dump(ini_get("opcache.enable_cli"));'

如果结果为 0,说明 Artisan 原本就没有使用 CLI OPcache。继续修改 PHP-FPM 的 OPcache 设置,通常不会改变 php artisan route:list 的运行结果。

持久禁用 CLI OPcache

确实需要长期关闭 CLI OPcache 时,可以在 CLI 的 OPcache 配置文件中加入:

opcache.enable_cli=0

实际文件可能是:

/etc/php/7.4/cli/conf.d/10-opcache.ini

保存配置后重新执行命令即可。每次运行命令都会启动新的 CLI 进程,因此不需要重启 Nginx 或 PHP-FPM。

在 Debian 中,还可以禁用 CLI SAPI 的 OPcache 模块链接:

sudo phpdismod -s cli opcache

恢复时运行:

sudo phpenmod -s cli opcache

phpdismod 和 phpenmod 是否可用,取决于 PHP 是否由 Debian 的打包系统安装。如果 PHP 来自第三方仓库或自行编译,应直接修改 php --ini 列出的配置文件。

为 Nginx 请求禁用 OPcache

Nginx 本身不运行 PHP。要影响 Web 请求,需要修改 PHP-FPM 的配置。修改 Nginx 配置或 Apache 的 .htaccess 都不会关闭 PHP-FPM 的 OPcache。

在 PHP-FPM 使用的 php.ini 或 OPcache 配置文件中设置:

opcache.enable=0
opcache.enable_cli=0

opcache.enable_cli 对 PHP-FPM 没有实际影响,保留该设置可以让配置意图更明确。

随后重启对应的 PHP-FPM 服务:

sudo systemctl restart php7.4-fpm

一般不需要重启 Nginx。修改完成后,可以创建一个仅允许本机访问的临时诊断页面:

<?php
var_dump(
    PHP_SAPI,
    php_ini_loaded_file(),
    php_ini_scanned_files(),
    ini_get('opcache.enable'),
    ini_get('opcache.preload'),
    opcache_get_status(false)
);

检查结束后应删除这个文件,以免公开配置路径和 PHP 环境信息。

如果系统中安装了多个 PHP 版本,还要确认 Nginx 的 FastCGI 配置连接了哪个套接字,例如:

fastcgi_pass unix:/run/php/php7.4-fpm.sock;

否则,即使修改了 PHP 7.4 的配置,Nginx 也可能实际连接着另一个版本的 PHP-FPM。

检查 PHP 7.4 预加载配置

PHP 7.4 引入的 preload 与普通 OPcache 缓存机制不同。预加载脚本会在 PHP-FPM 启动时载入类,这些类会一直保留到 PHP-FPM 进程重启。

检查相关配置:

php -i | grep -E 'opcache.preload|opcache.preload_user'

再搜索配置文件:

grep -R "opcache.preload" /etc/php/7.4 2>/dev/null

如果找到类似配置:

opcache.preload=/path/to/preload.php

可以先将它注释掉:

;opcache.preload=/path/to/preload.php

然后重启 PHP-FPM:

sudo systemctl restart php7.4-fpm

对于已经预加载的类,单独执行 opcache_reset() 通常不足以完成排查,重启 PHP-FPM 更可靠。同时检查 preload 脚本是否通过 require 再次加载了已由 Composer 管理的类文件。

PHP-FPM 的 preload 不会直接影响独立启动的 php artisan CLI 进程。如果错误始终出现在 Artisan 中,应先检查自动加载配置和重复定义。

排查重复类定义

下面的错误:

Cannot declare class Acme, because the name is already in use

表示当前 PHP 进程中已经声明了同名类。仅凭这条错误,无法认定 Symfony 或 OPcache 存在缺陷。

先在项目中搜索类定义:

rg -n --glob '*.php' \
  'class\s+Acme\b|interface\s+Acme\b|trait\s+Acme\b|enum\s+Acme\b' \
  app src routes config database tests

PHP 7.4 不支持 enum。如果代码库还会在其他 PHP 版本上运行,可以保留这项搜索;对于只运行 PHP 7.4 的项目,主要检查 class、interface 和 trait。

还要搜索手工加载代码:

rg -n --glob '*.php' \
  'require|require_once|include|include_once' \
  app src routes config bootstrap

常见原因有:

  • 两个文件在相同命名空间中声明了 Acme。
  • 同一个文件既由 Composer 加载,又被手写的 require 加载。
  • composer.json 中的 autoload.files 与 PSR-4 自动加载范围重叠。
  • autoload.classmap 扫描了内容重复的目录。
  • 项目中同时保留了旧文件和重命名后的新文件。
  • 文件名只有大小写不同,例如 Acme.php 和 acme.php。
  • Git 中的大小写重命名没有正确反映到部署环境。
  • preload 脚本和运行时自动加载器载入了同一份类代码。
  • 项目安装或复制了两份包含相同类名的包。

检查 Composer 自动加载配置

先查看 composer.json 中的自动加载配置,例如:

{
  "autoload": {
    "psr-4": {
      "App\\": "app/"
    },
    "classmap": [
      "database/seeders",
      "database/factories"
    ],
    "files": [
      "app/helpers.php"
    ]
  }
}

避免用多个重叠目录扫描同一组类。下面的配置就可能引起重复或歧义:

{
  "autoload": {
    "psr-4": {
      "App\\": "app/"
    },
    "classmap": [
      "app/"
    ]
  }
}

重新生成自动加载文件时,可以开启详细输出:

composer dump-autoload -o -vvv

Composer 可能会报告 ambiguous class resolution。遇到多个文件定义同一个类的提示时,应删除重复文件,或调整 autoload、autoload-dev、exclude-from-classmap 等配置,确保该类只有一个来源。

也可以直接检查 Composer 生成的映射:

rg -n "Acme" vendor/composer/autoload_*.php

如果同一个类出现在多个映射或文件列表中,需要继续检查对应的 composer.json 配置和包目录。

修正配置或源文件后,再清理应用缓存并重建映射:

php artisan optimize:clear
composer dump-autoload -o
php artisan route:list

optimize:clear 只能清除应用缓存,dump-autoload 只能重建自动加载映射。它们无法解决源代码中存在两个同名类的问题。

根据堆栈找到第二次声明的位置

PHP 的致命错误通常会给出第二次声明所在的文件和行号。先打开该文件,确认完整命名空间:

<?php

namespace App\Example;

class Acme
{
}

这个类的完整名称是:

App\Example\Acme

接着搜索完整命名空间和类名:

rg -n -F 'namespace App\Example;' .
rg -n 'class\s+Acme\b' .

搜索整个项目会包含 vendor 和缓存目录,结果可能很多。可以先检查业务代码,再根据需要扩大范围:

rg -n --glob '*.php' 'class\s+Acme\b' app src packages modules

如果报错文件位于 Symfony 或 Laravel 生成的缓存目录,不要直接修改生成文件。应继续查找生成它的服务定义、代理类、路由缓存或容器配置。

注意类名大小写问题

PHP 比较类名时不区分大小写,而 Linux 文件系统通常区分文件名大小写。因此,下面两个声明仍会发生冲突:

class Acme {}
class ACME {}

如果只修改文件名的大小写,Git 在某些开发环境中可能无法按预期记录这次重命名。可以分两步操作:

git mv app/Acme.php app/Acme.tmp
git mv app/Acme.tmp app/Acme.php

实际操作时,应把目标替换成正确的最终文件名,并确保类名、命名空间、PSR-4 路径及所有引用的大小写一致。

推荐的排查顺序

先确认 Artisan 是否启用了 OPcache:

php -r 'var_dump(ini_get("opcache.enable"), ini_get("opcache.enable_cli"), ini_get("opcache.preload"));'

然后临时关闭 OPcache,再次运行命令:

php -d opcache.enable=0 -d opcache.enable_cli=0 artisan route:list

如果错误消失,再检查 CLI 配置、预加载脚本和异常的 OPcache 设置,并在重启相应进程后重新测试。

如果错误没有变化,按下面的顺序继续排查:

composer dump-autoload -o -vvv
rg -n --glob '*.php' 'class\s+Acme\b' app src packages modules
rg -n "Acme" vendor/composer/autoload_*.php
php artisan optimize:clear

最终应确保 Acme 在同一个 PHP 进程中只有一个明确的声明来源。只有确认错误确实由某项 OPcache 或 preload 配置触发,才需要考虑长期关闭相关功能。

备注:内容仅供参考。