ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP8.4的动态类常量怎么反射获取

PHP8.4的动态类常量怎么反射获取 前言先纠正标题里的一个版本事实动态类常量取值Dynamic class constant fetch也就是Foo::{$name}这种写法是 PHP 8.3 引入的不是 8.4。本文按 PHP 8.3 讲。这个需求本身很常见配置类里堆了几十个常量现在要按数据库里存的名字去取其中一个或者写一个通用的常量导出接口把某个类的常量全列出来。老代码通常这么干?php $value constant(ApiConfig:: . $name);能跑但字符串拼接出来的类名和常量名没有任何静态检查拼错了要等运行到那一行才知道IDE 不能跳转静态分析工具也看不见。PHP 8.3 给了一个原生语法同时反射 API 也能做同一件事——两条路的适用场景不一样。本文讲清三件事Foo::{$name}的语义与边界、ReflectionClass读常量的正确姿势、以及在枚举enum和类型化常量typed class constant上的特殊之处。一、PHP 8.3 之前动态取常量只有一条路类常量和类属性在语言层面一直被区别对待$obj-$prop早就可以动态Foo::$prop在 PHP 8.3 之前却不行。原因是常量参与常量表达式const expression求值编译期就要确定而动态名字天然破坏了这个前提。所以 8.3 之前只有这些办法写法最低版本特点constant(Foo::BAR)PHP 5.3 起支持类常量字符串全字符串无静态检查拼错只能运行期发现ReflectionClass::getConstant()PHP 5 起返回false表示不存在但和常量值就是 false无法区分ReflectionClass::getReflectionConstant()PHP 8.0 起返回ReflectionClassConstant对象信息最全get_defined_constants()一直有返回全局常量表类常量不在里面要传true拿分类也不含类常量Foo::{$name}PHP 8.3 起原生语法编译期就能检查类名是否存在注意最后一行Foo::{$name}的常见用法是常量名动态、类名写死。如果类名也来自变量别去猜花括号里能塞什么直接回到constant($class::$name)这条路可读性和兼容性都最好。二、Foo::{$name}的语义与边界花括号里是一个完整表达式可以拼接、可以调用函数、可以是$this的属性。下面这段可以直接保存成.php运行最低要求 PHP 8.3。?php declare(strict_types1); final class ApiConfig { public const string VERSION v2; // 类型化常量也是 PHP 8.3 引入 public const string BASE_PATH /api; protected const int TIMEOUT 5; private const string SECRET s3cr3t; } $name VERSION; echo ApiConfig::{$name}, PHP_EOL; // v2 // 表达式、拼接、函数调用都行 $suffix PATH; echo ApiConfig::{BASE_ . $suffix}, PHP_EOL; // /api echo ApiConfig::{strtoupper(version)}, PHP_EOL; // v2 // 常量不存在抛 Error不是返回 null try { echo ApiConfig::{NOPE}; } catch (Error $e) { echo 捕获: , $e-getMessage(), PHP_EOL; } // 可见性依然受限类外部读不到 protected / private 常量 try { echo ApiConfig::{TIMEOUT}; } catch (Error $e) { echo 捕获: , $e-getMessage(), PHP_EOL; }三个要点不存在就抛Error和constant()的行为不同constant()对不存在的类常量也是抛Error但对不存在的全局常量是抛Error并带一段更啰嗦的提示。所以动态取值前想先判断存在性得用defined(ApiConfig::NOPE)或反射的hasConstant()。可见性照旧生效。Foo::{$name}不能绕过private这一点比constant()更安全——constant()历史上在可见性上的行为变过好几轮不要用它来做内部常量读取器。类名是变量时另有写法。花括号语法解决的是常量名动态类名动态的场景请用constant($class::$name)或反射不要把两种动态混在一个表达式里出问题时很难判断是哪一层挂了。三、反射拿到的信息比一个值多得多反射这条路的价值在于枚举全部常量和看清每个常量的元信息。方法最低版本返回用途ReflectionClass::getConstants(?int $filter)过滤参数 PHP 8.0 起arraystring, mixed常量名到值的映射含继承来的常量ReflectionClass::getConstant(string $name)一直有mixed找不到返回false单点取值不能用返回false判断是否存在ReflectionClass::hasConstant(string $name)一直有bool判断存在性含继承来的ReflectionClass::getConstants()的对象版getReflectionConstants(?int $filter)PHP 7.1 起arrayReflectionClassConstant需要可见性、类型、声明类时用ReflectionClass::getReflectionConstant(string $name)PHP 8.0 起ReflectionClassConstantfalse$filter参数接受ReflectionClassConstant::IS_PUBLIC、IS_PROTECTED、IS_PRIVATE、IS_FINALIS_FINAL是 PHP 8.1 起可用的过滤值可以直接筛掉不需要的常量省掉一层if。再看ReflectionClassConstant这个对象本身方法最低版本说明getName()/getValue()PHP 7.1基本读取getDeclaringClass()PHP 7.1常量真正定义在哪个类——继承场景下非常关键getModifiers()/isPublic()/isProtected()/isPrivate()PHP 7.1可见性isFinal()PHP 8.1是否为final常量isEnumCase()PHP 8.1是否是枚举的 casegetType()PHP 8.3返回?ReflectionType类型化常量的类型这里有个反直觉的地方getConstants()和getReflectionConstants()返回的是继承后的全集父类的常量也会出现在子类的反射结果里。想知道常量到底写在哪个类只能靠getDeclaringClass()想只拿本类声明的得自己按声明类过滤。另外PHP 8.1 起反射默认就能读取私有成员setAccessible()已经是空操作。以前那句$const-setAccessible(true)现在写了也没害处但不要再指望它承担安全职责。四、枚举enum上的常量是另一套枚举的 case 在语言实现上确实像常量但反射里不要用getConstants()去拿 case那样拿到的东西不是ReflectionEnumUnitCase得不到 case 的名字与关联值。正确入口是ReflectionEnumPHP 8.1 引入配套的是枚举语法本身ReflectionEnum::getCases()返回 case 对象数组元素是ReflectionEnumUnitCase纯枚举或ReflectionEnumBackedCase有 backing type 的枚举。ReflectionEnum::hasCase(string $name)判断某个 case 是否存在比getCases()再遍历更直接。ReflectionEnum::isBacked()与getBackingType()判断是否有关联值、关联值类型是什么。有 backing type 时ReflectionEnumBackedCase::getBackingValue()取出关联值。对一个纯枚举unit enum调用getBackingValue()是错误的用之前先用isBacked()判断。五、实战一个常量导出器下面这份代码把前面几节拼起来既支持按名字取也支持列出全部还把枚举 case 单独区分开。最低版本 PHP 8.3用到了类型化常量和getType()。?php declare(strict_types1); enum HttpStatus: int { case Ok 200; case NotFound 404; } final class ApiConfig { public const string VERSION v2; public const string BASE_PATH /api; final public const int MAX_RETRY 3; protected const int TIMEOUT 5; private const string SECRET s3cr3t; } /** * 按名字取常量存在性用 hasConstant() 判断绝不用 getConstant() 的返回值判断 */ function readConstant(string $class, string $name): mixed { $ref new ReflectionClass($class); if (!$ref-hasConstant($name)) { throw new InvalidArgumentException(常量 {$class}::{$name} 不存在); } $const $ref-getReflectionConstant($name); if ($const false) { // hasConstant() 已经通过这里是理论上的兜底 throw new RuntimeException(无法反射 {$class}::{$name}); } return $const-getValue(); } /** * 列出类的常量不含枚举 case枚举请用 listEnumCases() * PHP 8.0 起的 $filter 参数可以直接筛掉私有常量 */ function listConstants(string $class): void { $ref new ReflectionClass($class); $constants $ref-getReflectionConstants( ReflectionClassConstant::IS_PUBLIC | ReflectionClassConstant::IS_PROTECTED ); printf( %s 的常量%d 个\n, $ref-getShortName(), count($constants)); foreach ($constants as $const) { // getType() 是 PHP 8.3 引入的未声明类型的常量返回 null $type $const-getType(); printf( %-12s %-10s %-6s 声明于 %-10s 值%s\n, $const-getName(), $type instanceof ReflectionType ? $type-getName() : (无类型), $const-isFinal() ? final : -, $const-getDeclaringClass()-getShortName(), var_export($const-getValue(), true) ); } } /** * 枚举 case 必须走 ReflectionEnum不要混进 getConstants() */ function listEnumCases(string $enum): void { $ref new ReflectionEnum($enum); printf( %s 的 case \n, $ref-getShortName()); printf(backing type: %s\n, $ref-isBacked() ? $ref-getBackingType()-getName() : (无)); foreach ($ref-getCases() as $case) { $backing $case instanceof ReflectionEnumBackedCase ? var_export($case-getBackingValue(), true) : (无关联值); printf(%-10s %s\n, $case-getName(), $backing); } } listConstants(ApiConfig::class); echo PHP_EOL; listEnumCases(HttpStatus::class); echo PHP_EOL; // 动态取值类名已知就用 8.3 的语法类名是变量就退回 constant() $name BASE_PATH; $class ApiConfig::class; echo ApiConfig::{$name}, PHP_EOL; // /api echo constant({$class}::{$name}), PHP_EOL; // /api echo readConstant(ApiConfig::class, VERSION), PHP_EOL; // v2输出大致如下text围栏里是纯粹的运行结果 ApiConfig 的常量4 个 VERSION string - 声明于 ApiConfig 值v2 BASE_PATH string - 声明于 ApiConfig 值/api MAX_RETRY int final 声明于 ApiConfig 值3 TIMEOUT int - 声明于 ApiConfig 值5 HttpStatus 的 case backing type: int Ok 200 NotFound 404 /api /api v2注意TIMEOUT是protected因为过滤器里带了IS_PROTECTED所以出现在列表里而SECRET是private被过滤掉了。常见坑点1. 用getConstant()的返回值判断常量是否存在❌ 错误写法?php if ($ref-getConstant(DEBUG) false) { // DEBUG 的值本来就是 false 时这里会被误判成不存在 echo DEBUG 常量不存在; }✅ 正确写法?php if (!$ref-hasConstant(DEBUG)) { echo DEBUG 常量不存在; return; } $value $ref-getConstant(DEBUG); // 这里才安全这是最典型的静默错判常量值是false、0、、null时全部中招。2. 在枚举上用getConstants()拿 case❌ 错误写法?php foreach ((new ReflectionClass(HttpStatus::class))-getConstants() as $name $value) { echo $name; // 拿到的东西不是 case 对象name 和 backing value 对不上 }✅ 正确写法?php foreach ((new ReflectionEnum(HttpStatus::class))-getCases() as $case) { echo $case-getName(), , var_export($case-getBackingValue(), true), PHP_EOL; }3. 少写花括号被当成静态属性❌ 错误写法?php $name VERSION; echo ApiConfig::$name; // 解析成静态属性 $VERSION报 Access to undeclared static property✅ 正确写法?php $name VERSION; echo ApiConfig::{$name}; // 花括号才是动态常量名$name里放的是常量名语法上却走了属性那条路报错信息里提的是静态属性很容易把人带偏去查属性定义。如果类名本身也来自变量别把两层动态叠在一个表达式里用constant({$class}::{$name})或反射出问题时才好定位。4. 以为类型化常量是更早版本就有的❌ 错误写法在 PHP 8.2 环境里写public const string VERSION v2;然后被语法错误搞懵。✅ 正确写法类型化类常量是PHP 8.3引入的8.2 及以下只能写public const VERSION v2;。同理ReflectionClassConstant::getType()也是 8.3 才有在 8.2 上调用会报方法不存在。5. 忘了继承来的常量❌ 错误写法写一个这个类定义了哪些常量的配置校验器直接用getConstants()的键做白名单结果父类常量被误判成本类常量。✅ 正确写法逐个判断声明类?php foreach ($ref-getReflectionConstants() as $const) { if ($const-getDeclaringClass()-getName() ! $ref-getName()) { continue; // 跳过继承来的 } // ... }6. 把setAccessible()当成权限开关❌ 错误写法以为不调用setAccessible(true)就读不到私有常量于是在循环里到处调用反过来又以为它是授权在某个分支里故意不调用试图保护私有常量。✅ 正确写法PHP 8.1 起反射默认就能读私有成员setAccessible()已经是空操作。私有常量对外真正的作用是防止被Foo::CONST直接访问反射这条路从一开始就是敞开的——不要把敏感信息放常量里再指望反射拦住。7. 用define()的第三参数做大小写不敏感的常量❌ 错误写法?php define(APP_MODE, prod, true); // PHP 8.0 起第三个参数已被移除✅ 正确写法define(APP_MODE, prod);并记住类常量一直是大小写敏感的。第三个参数在 PHP 8.0 被移除升级后代码会直接报错——这是升级时出现在define()那一行的经典报错。8. 动态取值前不判断存在性❌ 错误写法?php // 用户输入直接决定常量名 echo ApiConfig::{$_GET[key]};✅ 正确写法?php $key (string) ($_GET[key] ?? ); if (!defined(ApiConfig::class . :: . $key)) { throw new InvalidArgumentException(非法的常量名); } echo ApiConfig::{$key};$_GET里传进来的东西可能带::、带空格、带控制字符直接用会变成Error抛给用户看把内部结构泄露出去。总结需求推荐做法最低版本类名写死、常量名动态Foo::{$name}PHP 8.3类名也是变量constant({$class}::{$name})PHP 5.3先判断存在再取值hasConstant()getConstant()一直可用拿到类型/可见性/声明类getReflectionConstants()PHP 7.1过滤参数 8.0读类型化常量的类型ReflectionClassConstant::getType()PHP 8.3遍历枚举 caseReflectionEnum::getCases()PHP 8.1要点只有两句动态类常量取值是 PHP 8.3 的特性标题里的 8.4 是记错了判断常量存不存在永远用hasConstant()不要用getConstant()的返回值。把这两条记住反射读常量这条路上几乎不会再踩坑。
返回列表