先明确一个基本判断:PHP 里的 static 变量,既不是全局变量,也不是跨请求的持久化工具。它只在单次脚本执行的生命周期内有效——FastCGI 模式下每个 worker 各自维护一份,CLI 模式下每次 php script.php 都是全新副本。这个特性从 PHP 4 延续到 PHP 8.3,从未变过。可惜不少人总想用它去模拟“全局状态”,结果踩了一堆坑。
下面从三个角度拆开来说:函数内的 static 变量、类中的 static 属性,以及为什么有人坚持用它做全局替代但往往翻车。
函数内 static 变量:适合单次请求内的状态保持
就实际场景来说,函数内的 static 变量是最稳妥的选择。它不污染全局命名空间,又能让函数在多次调用间保留一个值。安全且干净。
static变量的初始化只执行一次,哪怕初始化表达式里带了函数调用(比如file_get_contents()或time()),也只在首次调用时触发。后续调用直接复用缓存的值。- PHP 8.3 对初始化表达式更宽容了,你可以写
static $config = ['host' => $_ENV['DB_HOST'] ?? 'localhost'];。不过别误会,$_ENV本来就是运行时变量,不是编译期常量,所以这行实际相当于惰性赋值,行为是稳定的。 - 但得留个心眼:别在条件块里重复声明
static变量,比如if (...) { static $x = 1; }。语法虽然能过,但语义并不可靠,不同 SAPI 下的表现不一致,调试起来能让你崩溃。 - 调试时最容易犯的错误是“为什么这个值没变”?其实大概率是你根本没触发首次调用,或者换了一个 worker / CLI 进程,查错了上下文。
类中 static 属性:封装比裸 global 强,但仍是 per-request
类级别的 static 属性不是 global 的平替,它本质上是带访问控制的“模块内共享变量”。它的好处是解决了命名冲突和组织问题,但跟状态持久化毫无关系。
- 写
MyService::$cache和写$GLOBALS['cache']完全不同:前者受类作用域约束,后者裸奔且容易被任意代码覆盖。这是封装性的提升,不是状态能力的升级。 - 子类继承时要特别小心
self::和static::的绑定差异:self::$count永远指向父类定义的变量,static::$count才会按实际调用类来查找。继承链一深,计数很容易错乱。 - 千万别指望它存用户会话数据,比如
User::$current。HTTP 请求之间完全隔离,FPM 多 worker 下,每个 worker 各自维护一份,跨请求读取根本拿不到。 - 如果需要在类加载时就初始化,必须手动触发(比如在第一个静态方法里加
self::init())。PHP 不提供真正的静态构造函数,别指望自动执行。
为什么有人坚持用 static 模拟全局?陷阱在哪
核心动机其实是想规避 global 关键字带来的维护灾难——毕竟全局变量裸奔,代码里到处都是 global $db 太可怕了。但不少人把“封装性提升”误当成“状态能力升级”,结果掉进几个常见的陷阱里:
- 误以为
static能跨请求共享。实际上它只在单次脚本生命周期存活,重启服务或换 worker 就清空。想跨请求?得用 APCu、Redis 或文件缓存。 - 混淆单例模式与
static属性。用new MyService()创建实例,跟读写MyService::$counter是两套生命周期,混用会导致状态归属混乱。 - 单元测试中残留状态。每个 test 方法都应当重置
static属性(比如MyService::$counter = 0;),否则测试间会相互污染,结果不可靠。 - 依赖
static做配置中心。正确做法是用外部存储(APCu / Redis)加上初始化逻辑兜底,static只作为内存缓存层,而非唯一信源。
真正难处理的从来不是怎么写 static,而是搞清它“活多久、谁可见、谁负责清理”。PHP 的 request-per-process 模型决定了:任何想绕过这个模型去模拟长期全局状态的尝试,最终都会撞上进程隔离墙。懂了这个,再写 static 就不会跑偏了。
