PHP 枚举 Enum 怎么使用?用更清晰的方式管理固定状态和类型
在企业软件、SaaS、后台系统和API开发中,有一类数据看起来只是几个固定值,实际上却直接参与业务逻辑。例如用户可能处于active、inactive、disabled几种状态,订单可能处于pending、paid、cancelled等阶段,权限可能分为read、write、admin。传统PHP项目经常使用整数常量、字符串常量或者数据库字段直接保存这些值,代码能够运行,但随着项目规模扩大,状态值会逐渐散落在控制器、Model、Service和数据库查询中。
PHP 8.1引入Enum以后,这类固定集合可以被建模成真正的类型。枚举不仅可以表达“有哪些合法状态”,还可以让函数参数、返回值、match分支和业务方法围绕这个类型工作,从而减少字符串和整数在系统各处自由流动的问题。
PHP Enum从哪个版本开始支持
原稿中“PHP 7.1引入Enum”这一点是错误的。PHP原生枚举是在PHP 8.1正式加入的,因此使用enum语法的项目最低需要PHP 8.1。
最基本的枚举可以这样定义:
enum UserStatus
{
case Active;
case Inactive;
case Disabled;
}
这里的Active、Inactive和Disabled不是普通字符串,也不是整数常量,而是UserStatus类型的枚举Case。
使用时:
$status = UserStatus::Active;
此时$status是一个UserStatus对象实例,而不是字符串"active"。
因此下面的类型声明是有意义的:
function updateUserStatus(UserStatus $status): void
{
// ...
}
调用者必须传入UserStatus的Case。如果错误地传入一个普通字符串或整数,在严格的类型检查环境下就会暴露类型问题。
这也是Enum与传统常量最大的区别之一:传统常量主要提供一个名字和值,而Enum首先建立的是一个独立的类型。
Backed Enum适合数据库中的状态字段
实际SaaS系统通常需要把状态保存到MySQL、PostgreSQL等数据库中,因此单纯的Unit Enum还不够方便。
PHP提供了Backed Enum,可以让每个Case关联一个字符串或整数值。
例如:
enum UserStatus: string
{
case Active = 'active';
case Inactive = 'inactive';
case Disabled = 'disabled';
}
现在:
$status = UserStatus::Active;
echo $status->value;
结果是:
active
这里需要区分两个概念。
UserStatus::Active是Enum Case,也就是一个UserStatus对象;$status->value才是与数据库、API或者其他外部系统交互时使用的底层字符串。
因此数据库字段可以保存active,PHP业务层则可以使用:
UserStatus::Active
这样可以把数据库中的原始值和业务代码中的类型区分开。
from和tryFrom解决外部值转换问题
Backed Enum还有两个非常实用的方法:from()和tryFrom()。
如果确定外部值一定合法,可以使用:
$status = UserStatus::from('active');
它会得到:
UserStatus::Active
如果传入不存在的值,例如:
$status = UserStatus::from('unknown');
则会抛出ValueError。
对于来自用户输入、数据库旧数据或者第三方API的数据,通常更适合:
$status = UserStatus::tryFrom('unknown');
找不到对应Case时,tryFrom()返回null,而不是直接抛出异常。
例如:
$status = UserStatus::tryFrom($input);
if ($status === null) {
// 非法状态值
}
这在API参数验证和旧系统数据迁移中尤其有用。
Enum与match组合后业务逻辑会更加清晰
Enum最适合与PHP 8.0加入的match表达式结合。
正确写法应该是:
$message = match ($status) {
UserStatus::Active => '用户已激活',
UserStatus::Inactive => '用户未激活',
UserStatus::Disabled => '用户已禁用',
};
这里不能写成:
UserStatus::Active => echo '用户已激活'
因为match的每个分支需要产生一个表达式结果,而echo不是这种用法下的返回表达式。
Enum和match结合还有一个非常重要的工程价值:状态集合发生变化时,业务代码更容易发现遗漏。
例如以后新增:
case Pending;
那么原来的match如果没有处理Pending,在运行时就可能出现未匹配分支的问题。
相比散落在代码中的:
if ($status === 'active') {
...
} elseif ($status === 'inactive') {
...
}
Enum能够让状态集合拥有更加明确的边界。
Enum可以定义自己的方法
Enum并不是只能存储几个Case。
它可以定义方法:
enum UserStatus: string
{
case Active = 'active';
case Inactive = 'inactive';
case Disabled = 'disabled';
public function label(): string
{
return match ($this) {
self::Active => '已激活',
self::Inactive => '未激活',
self::Disabled => '已禁用',
};
}
}
调用:
$status = UserStatus::Active;
echo $status->label();
这样与状态相关的显示逻辑就可以集中在Enum内部,而不是在几十个Controller中重复写。
当然,也不能把所有业务逻辑都塞进Enum。简单的状态描述、状态分类、是否允许某种操作等与状态本身高度相关的逻辑适合放进去;如果涉及数据库查询、外部API、复杂工作流或者多个领域对象,则应该继续放在Service等业务层。
Enum可以遍历所有Case
如果需要获取一个枚举的所有Case,可以使用:
foreach (UserStatus::cases() as $status) {
echo $status->value;
}
cases()会返回这个Enum定义的所有Case。
这对于后台管理页面生成状态选项、API文档、验证逻辑或者测试数据都很方便。
例如:
foreach (UserStatus::cases() as $status) {
echo '';
}
这样新增一个状态以后,前端选项可以通过统一逻辑自动生成,而不需要手工维护一组字符串。
Enum也可以实现接口
如果多个Enum需要提供统一能力,可以让它们实现接口。
例如:
interface HasLabel
{
public function label(): string;
}
然后:
enum UserStatus: string implements HasLabel
{
case Active = 'active';
case Disabled = 'disabled';
public function label(): string
{
return match ($this) {
self::Active => '已激活',
self::Disabled => '已禁用',
};
}
}
这样其他代码就可以依赖接口,而不是依赖某一个具体Enum。
这也是Enum逐渐从“固定值列表”变成业务类型的重要原因。
Enum与数据库设计不能混为一谈
PHP Enum并不意味着数据库一定要使用MySQL的ENUM字段。
例如PHP可以使用:
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Cancelled = 'cancelled';
}
数据库完全可以使用:
status VARCHAR(20) NOT NULL
然后在应用层将数据库值转换为OrderStatus。
对于很多SaaS系统,这种设计比把业务状态完全绑定到数据库原生ENUM更加灵活。
如果业务状态变化频繁,数据库CHECK约束、外键字典表或者应用层Enum也可以根据具体需求组合使用。
关键不是“必须使用哪一种”,而是要明确哪一层负责约束什么。
数据库负责数据完整性,PHP Enum负责应用程序类型和业务代码中的状态表达,两者并不是互相替代的关系。
Enum比常量好在哪里
传统代码可能这样定义:
class UserStatus
{
public const ACTIVE = 'active';
public const INACTIVE = 'inactive';
public const DISABLED = 'disabled';
}
然后:
$status = UserStatus::ACTIVE;
这种方式当然仍然有效,而且在旧版本PHP项目中非常常见。
问题在于UserStatus::ACTIVE本身不是一个UserStatus类型。数据库查询、HTTP请求、数组和业务逻辑之间仍然可能到处传递字符串。
Enum则可以直接写:
function suspendUser(UserStatus $status): void
{
...
}
这让函数接口本身表达了“这里接受的是哪一种状态”。
静态分析工具、IDE自动补全以及代码重构工具也能够更准确地理解这种关系。
因此Enum的价值并不是“代码更短”,而是让固定集合成为代码中的类型边界。
Enum也不是所有固定值都应该使用
Enum同样不能滥用。
如果一个值本身没有固定集合,例如用户昵称、商品名称、电话号码、金额,就不应该因为想要“类型安全”而建立Enum。
如果状态来自动态配置,而且管理员可以随时增加新的类型,那么Enum也未必适合,因为Enum的Case属于代码的一部分,需要经过代码部署才能增加。
还有一种常见误区,是把Enum当成数据库业务规则的全部实现。
例如订单状态从pending变成paid,并不意味着只要Enum里存在这两个Case,就允许任何地方随意修改。
真正的订单状态转换规则可能是:
pending → paid
pending → cancelled
paid → refunded
而:
cancelled → paid
可能根本不允许。
Enum负责描述“有哪些状态”,状态机或者业务Service负责描述“状态之间允许怎样转换”。这两个概念需要分开。
在现代PHP项目中,Enum更适合放在领域模型附近
对于一个SaaS项目,可以把类似用户状态、订阅状态、订单状态、支付状态、权限类型等稳定的固定集合定义成Enum。
例如:
enum SubscriptionStatus: string
{
case Trialing = 'trialing';
case Active = 'active';
case PastDue = 'past_due';
case Cancelled = 'cancelled';
}
Service层接收:
function changeSubscriptionStatus(
SubscriptionStatus $status
): void {
// ...
}
数据库层保存:
$status->value
从数据库读取以后:
$status = SubscriptionStatus::from($row['status']);
这样数据库、Repository、Service和Controller之间就形成了清晰的类型边界。
如果项目使用ORM,例如Laravel、Symfony生态中的相关组件,也可以进一步利用框架对Backed Enum的支持,让数据库字段与PHP Enum之间进行自动转换,但具体方式应该按照所使用的ORM版本实现,而不是把所有框架都当成同一种行为。
PHP Enum真正有价值的地方,不是把1、2、3换成几个漂亮的名字,而是把原本散落在程序里的“允许值集合”提升成一个明确的类型。对于大型PHP项目,这种边界能够让IDE、静态分析器、测试代码以及开发人员共同理解同一套业务状态。
从传统常量到Enum,变化的也不只是语法。以前程序员需要记住“这个字符串到底可以写什么”,现在可以让类型系统直接参与约束。数据库仍然保存外部值,API仍然需要验证输入,Service仍然负责业务规则,而Enum负责把固定状态集合表达清楚。对于PHP 8.1及以上的新项目,只要业务确实存在稳定的固定值集合,Enum通常比散落的字符串和整数常量更值得优先考虑。
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP