PHP trait中静态属性的合理处理方案:单元测试全局标记需求的实现困惑
我完全懂你的痛点——单元测试里重复执行数据库创建操作真的太浪费时间了,想用trait的静态属性做个全局标记来避免重复执行,结果却踩了PHP8的警告坑,确实挺闹心的。
先帮你理清楚问题根源:当你在trait里定义静态属性$hasRun,每个使用这个trait的测试类(比如SomeTestClass、SomeOtherTestClass)都会得到这个属性的独立副本,所以self::$hasRun其实是每个测试类自己的标记,根本达不到全局共享的目的;而直接写CreateDatabase::$hasRun触发警告,是因为PHP8开始明确不推荐直接访问trait的静态成员——trait本质是代码复用的工具,不是用来单独承载全局状态的容器。
下面给你几个符合PHP规范、能完美解决需求的方案:
方案一:用独立的全局状态类存储标记
最直观也最规范的做法,是把全局标记单独放到一个专门的类里,trait只负责调用这个类的属性:
// 专门存测试全局标记的类 class TestGlobalState { public static bool $databaseCreated = false; } trait CreateDatabase { protected function runDb(): void { if (!TestGlobalState::$databaseCreated) { // 执行数据库创建操作... echo "创建数据库\n"; } TestGlobalState::$databaseCreated = true; } } // 测试类1 class SomeTestClass { use CreateDatabase; public function canRunSomeTest() { $this->runDb(); // 测试逻辑... } } // 测试类2 class SomeOtherTestClass { use CreateDatabase; public function canRunAnotherTest() { $this->runDb(); // 测试逻辑... } }
这样不管多少个测试类使用trait,都会共享TestGlobalState::$databaseCreated这个全局标记,而且完全符合PHP的语法规范,不会触发任何警告。
方案二:在trait内部定义静态内部类存标记
如果不想单独新建一个类,可以在trait里定义一个私有静态内部类来承载全局状态:
trait CreateDatabase { // 私有内部类,专门存全局标记 private static class FlagHolder { public static bool $hasRun = false; } protected function runDb(): void { if (!self::FlagHolder::$hasRun) { // 执行数据库创建操作... echo "创建数据库\n"; } self::FlagHolder::$hasRun = true; } }
这个内部类是和trait绑定的,所有使用该trait的测试类都会共享FlagHolder::$hasRun,而且因为是通过trait的上下文访问内部类,完全不会触发PHP的废弃警告。
方案三:利用PHPUnit的测试基类(如果用PHPUnit的话)
如果你的项目用的是PHPUnit,其实可以直接借助它的生命周期方法,用一个抽象基类来实现全局建库:
use PHPUnit\Framework\TestCase; abstract class DatabaseTestCase extends TestCase { protected static bool $databaseCreated = false; public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); if (!self::$databaseCreated) { // 执行数据库创建操作... echo "创建数据库\n"; self::$databaseCreated = true; } } } // 所有需要数据库的测试类都继承这个基类 class SomeTestClass extends DatabaseTestCase { /** * @test */ public function canRunSomeTest() { // 测试逻辑,数据库已经在setUpBeforeClass里创建好了 } }
setUpBeforeClass会在每个测试类的第一个测试方法执行前运行一次,而基类的静态属性$databaseCreated会被所有子类共享,所以数据库只会创建一次,完全不需要手动调用runDb方法,更贴合PHPUnit的使用习惯。
总的来说,方案一和方案二通用性更强,适合任何测试框架;方案三是PHPUnit专属的更优雅的实现,你可以根据自己的项目情况选择。
备注:内容来源于stack exchange,提问作者kmuenkel

