全局作用域如何移除?

解读

“全局作用域如何移除”在国内 PHP 面试里并不是问“把全局作用域从 PHP 引擎里删掉”,而是考察候选人能否识别并消除代码中“不必要的全局变量污染”,让业务逻辑、单元测试、协程(Swoole/Fiber)上下文更安全、可维护。
面试官想听到的是:

  1. 先定位:哪些变量/常量/配置被“放”在了全局作用域;
  2. 再治理:用何种机制把它们“收”回来,做到“无全局”或“最小全局”;
  3. 最后兜底:如果框架/扩展必须依赖全局,如何隔离、锁定、及时清理。

知识点

  1. 超全局数组:GLOBALSGLOBALS、_GET、POST_POST、_SERVER …
  2. 用户自定义全局变量:函数外定义的 configconfig、db、$logger …
  3. 常量、静态属性、单例:define()、const、static::$instance
  4. 垃圾回收与符号表:zend_hash_del、unset、gc_collect_cycles
  5. 封装手段:依赖注入容器、Registry 模式、Context 对象、闭包绑定
  6. 协程安全:Swoole\Coroutine::getContext()、Swow\Coroutine::getId()
  7. 自动加载与 Composer:psr-4、files 字段过早污染全局
  8. 测试隔离:PHPUnit 的 @backupGlobals、@backupStaticAttributes
  9. OPcache 与预加载:opcache.preload 脚本里如果泄漏全局,FPM 重启前无法清理
  10. 安全规范: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” 检查,列出候选清单。

第二步:迁移

  1. 配置型全局 → 迁入专用 Config 类,使用 ArrayAccess 延迟加载;
  2. 数据库句柄 → 通过工厂注册到 DI 容器,构造函数注入;
  3. 工具型函数 → 用静态类方法或 namespaced function 包裹,避免 define();
  4. 必须跨协程共享的数据 → 用 Swoole\Table / APCu / Redis 替代 $GLOBALS;
  5. 单例 → 把 static::instance放到私有闭包,暴露接口get(ContainerInterfaceinstance 放到私有闭包,暴露接口 get(ContainerInterface c),实现“容器托管单例”。

第三步:清理

  1. 在请求生命周期末尾(如 Laravel 的 TerminableMiddleware、Swoole 的 onRequest 之后)显式 unset($GLOBALS['_custom']) 并 gc_collect_cycles();
  2. 对 CLI 常驻脚本,每 N 次循环主动调用 opcache_invalidate() 与 gc_collect_cycles(),防止符号表膨胀;
  3. 单元测试里开启 @backupGlobals enabled,确保 case 之间零泄漏;
  4. 上线前做“全局变量 diff”灰度:在 auto_prepend_file 里记录 get_defined_vars() 的 md5,两次采样不一致立即告警。

做到以上,即可把“全局作用域”从业务代码里“移除”,让 PHP 进程在 FPM/CLI/Swoole 多种模式下都保持干净、可测、可水平扩展。

拓展思考

  1. 如果历史项目用 define() 定义了 200+ 常量,如何批量迁移?
    答:写脚本扫描 token(T_STRING),生成 class Constants { const XXX = ...; },再用 Rector 做自动化替换;上线时采用“双读”策略:新代码读类常量,旧代码读 define,灰度 3 天后下线 define。
  2. 预加载脚本里一旦 require 了带全局变量的文件,FPM 重启前无法清理,怎么解?
    答:把预加载拆成“纯类映射”与“配置映射”两步,后者不预加载,改用懒加载;或者把变量封装到匿名函数内部,预加载只注册函数,不执行。
  3. 在 RoadRunner/FrankenPHP 等常驻模式里,Worker 复用导致全局静态数组累加,如何隔离?
    答:利用 Worker 的 reset() 钩子,每次请求结束清空静态属性;或把数据放到 spiral/goridge 的独立进程内存,彻底脱离 PHP 全局。