全局作用域如何移除?
解读
“全局作用域如何移除”在国内 PHP 面试里并不是问“把全局作用域从 PHP 引擎里删掉”,而是考察候选人能否识别并消除代码中“不必要的全局变量污染”,让业务逻辑、单元测试、协程(Swoole/Fiber)上下文更安全、可维护。
面试官想听到的是:
- 先定位:哪些变量/常量/配置被“放”在了全局作用域;
- 再治理:用何种机制把它们“收”回来,做到“无全局”或“最小全局”;
- 最后兜底:如果框架/扩展必须依赖全局,如何隔离、锁定、及时清理。
知识点
- 超全局数组:_GET、_SERVER …
- 用户自定义全局变量:函数外定义的 db、$logger …
- 常量、静态属性、单例:define()、const、static::$instance
- 垃圾回收与符号表:zend_hash_del、unset、gc_collect_cycles
- 封装手段:依赖注入容器、Registry 模式、Context 对象、闭包绑定
- 协程安全:Swoole\Coroutine::getContext()、Swow\Coroutine::getId()
- 自动加载与 Composer:psr-4、files 字段过早污染全局
- 测试隔离:PHPUnit 的 @backupGlobals、@backupStaticAttributes
- OPcache 与预加载:opcache.preload 脚本里如果泄漏全局,FPM 重启前无法清理
- 安全规范:GB/T 38674-2020《信息安全技术 代码安全指南》要求“最小化全局变量生命周期”
答案
分三步落地“移除全局作用域”:
第一步:扫描
$ grep -rn '^\s*\$[a-zA-Z_]' --include='*.php' . | grep -v 'function\|class' | awk -F: '{print $1}' | sort | uniq -c | sort -nr
拿到“在函数/类外直接赋值”的文件列表,结合 PHPStorm 的 “Unused global variable” 检查,列出候选清单。
第二步:迁移
- 配置型全局 → 迁入专用 Config 类,使用 ArrayAccess 延迟加载;
- 数据库句柄 → 通过工厂注册到 DI 容器,构造函数注入;
- 工具型函数 → 用静态类方法或 namespaced function 包裹,避免 define();
- 必须跨协程共享的数据 → 用 Swoole\Table / APCu / Redis 替代 $GLOBALS;
- 单例 → 把 static::c),实现“容器托管单例”。
第三步:清理
- 在请求生命周期末尾(如 Laravel 的 TerminableMiddleware、Swoole 的 onRequest 之后)显式 unset($GLOBALS['_custom']) 并 gc_collect_cycles();
- 对 CLI 常驻脚本,每 N 次循环主动调用 opcache_invalidate() 与 gc_collect_cycles(),防止符号表膨胀;
- 单元测试里开启 @backupGlobals enabled,确保 case 之间零泄漏;
- 上线前做“全局变量 diff”灰度:在 auto_prepend_file 里记录 get_defined_vars() 的 md5,两次采样不一致立即告警。
做到以上,即可把“全局作用域”从业务代码里“移除”,让 PHP 进程在 FPM/CLI/Swoole 多种模式下都保持干净、可测、可水平扩展。
拓展思考
- 如果历史项目用 define() 定义了 200+ 常量,如何批量迁移?
答:写脚本扫描 token(T_STRING),生成 class Constants { const XXX = ...; },再用 Rector 做自动化替换;上线时采用“双读”策略:新代码读类常量,旧代码读 define,灰度 3 天后下线 define。 - 预加载脚本里一旦 require 了带全局变量的文件,FPM 重启前无法清理,怎么解?
答:把预加载拆成“纯类映射”与“配置映射”两步,后者不预加载,改用懒加载;或者把变量封装到匿名函数内部,预加载只注册函数,不执行。 - 在 RoadRunner/FrankenPHP 等常驻模式里,Worker 复用导致全局静态数组累加,如何隔离?
答:利用 Worker 的 reset() 钩子,每次请求结束清空静态属性;或把数据放到 spiral/goridge 的独立进程内存,彻底脱离 PHP 全局。