之前维护一个老商城项目,里面订单状态的代码写得那叫一个“壮观”:各种if else套着switch,每个状态还对应不同的操作按钮、不同的通知消息。加一个新状态,我得翻遍十几个文件去改判断条件。后来我看到PHP 8.1发布了枚举(Enum),第一反应就是“这不就是给这种场景量身定做的吗?” 我花了一个周末,把订单状态相关的业务全部切换成了原生枚举,效果出乎意料的好——代码量少了一大半,而且逻辑清楚到闭着眼睛都能改。
旧的代码长什么样
原来的订单表里有个字段叫status,存的是数字:0待付款,1已付款,2已发货,3已完成,4已取消。听起来很清晰,但到处散落着魔法数字。你要查询待付款订单,就得写->where('status', 0),鬼知道0是啥。更别提在模板里判断状态显示什么按钮:
if ($order['status'] == 0) {
echo '立即付款';
} elseif ($order['status'] == 1) {
echo '申请退款';
} elseif ($order['status'] == 2) {
echo '确认收货';
} elseif ($order['status'] == 3) {
echo '删除订单';
} elseif ($order['status'] == 4) {
echo '再次购买';
} else {
echo '未知状态';
}
然后控制器里也有一堆判断:
if ($order['status'] == 0) {
// 发送待付款通知
$message = '您有一笔订单待付款';
} elseif ($order['status'] == 1) {
// 发送已付款通知
$message = '您已付款成功';
} elseif ...
这还只是其中两个文件,实际上还有取消逻辑、退款逻辑、订单列表筛选、后台操作权限……全部都在跟数字硬碰硬。每次需求变更,比如“加一个状态 5 表示已发货但被退回”,我就要全局搜索数字 0-4,逐个改。我当时被逼得写了个常量类,但还是不够优雅,因为常量只是有名字的数字,依然没法封装行为。
用Enum替换数字
PHP 8.1的枚举是真正的枚举,它不光能表示常量,还可以带方法、带内部状态。我首先在app/enums目录下新建了一个OrderStatus.php:
<?php
namespace appenums;
enum OrderStatus: int
{
case PENDING = 0; // 待付款
case PAID = 1; // 已付款
case SHIPPED = 2; // 已发货
case COMPLETED = 3; // 已完成
case CANCELLED = 4; // 已取消
}
然后我把原来条件里所有的数字都替换成语义化的枚举名称。之前的查询变成了:
// 查询待付款订单
$orders = Db::name('order')->where('status', OrderStatus::PENDING->value)->select();
模板里的判断也清晰了:
if ($order['status'] == OrderStatus::PENDING->value) {
echo '立即付款';
} elseif ($order['status'] == OrderStatus::PAID->value) {
echo '申请退款';
} elseif ($order['status'] == OrderStatus::SHIPPED->value) {
echo '确认收货';
}
但这只是第一步,因为if else还是没减少。真正的威力在于,我可以在枚举内部定义方法,把和状态相关的逻辑都收拢到枚举类里。
用 Enum 替代状态判断
我开始在OrderStatus里加方法。比如给每个状态一个“可执行操作”的列表:
public function actions(): array
{
return match ($this) {
self::PENDING => ['pay'],
self::PAID => ['refund'],
self::SHIPPED => ['confirm'],
self::COMPLETED => ['delete'],
self::CANCELLED => ['rebuy'],
};
}
然后模板中不再需要一堆if,只要:
foreach (OrderStatus::from($order['status'])->actions() as $action) {
// 根据$action显示对应的按钮
echo $action;
}
这样要加一个“已发货但退回”的状态,只需要在枚举中加一个CASE,然后在actions里加一项。所有使用这个枚举的地方自动生效,不用满世界找那些elseif。
还有一个常见需求:判断订单状态是否允许某种操作。比如取消订单,只允许待付款状态取消。我在枚举里加一个canCancel()方法:
public function canCancel(): bool
{
return $this === self::PENDING || $this === self::PAID;
}
这样在控制器里就变成了:
if (OrderStatus::from($order['status'])->canCancel()) {
// 执行取消
}
再也不用去记哪个数字能取消哪个不能。
结合状态模式:让订单自己走流程
我进一步把状态的流转逻辑也封装进去。比如付款成功后,状态从PENDING变成PAID,定义一个transition()方法:
public function next(): OrderStatus
{
return match ($this) {
self::PENDING => self::PAID,
self::PAID => self::SHIPPED,
self::SHIPPED => self::COMPLETED,
default => $this,
};
}
这样“订单状态推进”这个动作变成了一个方法调用,而不是在业务逻辑里随便赋值 $order['status'] = $order['status'] + 1,那种加一的操作非常脆弱(万一状态之间不是连续数字呢?)。
我还给状态加了一个label()方法,用于后台列表直接显示状态名称,不用再去搞一个映射表:
public function label(): string
{
return match ($this) {
self::PENDING => '待付款',
self::PAID => '已付款',
self::SHIPPED => '已发货',
self::COMPLETED => '已完成',
self::CANCELLED => '已取消',
};
}
这样前端调用OrderStatus::from($status)->label()就能直接拿到中文名称。
从数据库读取数值时的转换
现在最爽的是从数据库取出来的status字段是整数,我可以直接用OrderStatus::from($order['status'])转换。如果状态值非法,from()会抛异常,但也可以在模型里做一个自动转换,比如在模型里定义一个属性访问器:
class Order extends Model
{
public function getStatusAttr($value)
{
return OrderStatus::from($value);
}
}
这样在PHP代码里直接拿到的就是枚举对象,而不是数字。不过对于模板来说可能不太友好,所以我在需要输出数字的地方用->value取回数字即可。
实际性能有影响吗?
你可能会担心每次调用枚举方法会有点开销。实际上PHP 8.1的枚举就是一种特殊的对象,它的值类型是int或string,底层是单例对象,内存占用很小。每次比较都是同一个对象实例,所以性能非常高。我测试过线上压力,切换枚举后响应时间几乎没变化。
用枚举后我把原来的状态常量类删了
以前为了少用魔法数字,我建了一个OrderStatusConst.php,里面定义了一堆const整数。现在有了枚举,这个类彻底成了历史。我把它删掉,所有引用它的地方一一替换成枚举。全局搜索一下,把OrderStatusConst::STATUS_PENDING替换成OrderStatus::PENDING->value,过程并不复杂。而且IDE能自动补全,也不会写错常量名。
处理一个真实业务:订单超时自动取消
我还写了一个定时任务,去把超过30分钟未付款的订单自动取消。以前我的代码是:
Db::name('order')->where('status', 0)->where('create_time', '<', time()-1800)->update(['status' => 4]);
现在改成:
Db::name('order')->where('status', OrderStatus::PENDING->value)->where('create_time', '<', time()-1800)->update(['status' => OrderStatus::CANCELLED->value]);
区别不大,但至少一眼能看懂在干嘛。更关键的是,如果将来状态编号变化(比如PENDING变成5),我只要改枚举定义,所有代码都不用动。
如果状态需要存储字符串?
有些项目喜欢用字符串状态,比如’pending’,’paid’。枚举同样可以支持,定义成enum OrderStatus: string即可。用法基本相同。
枚举的局限:不能实现状态机全部能力
如果你的订单状态流转非常复杂,比如有分支、子状态、闭环,那枚举可能不够描述整个状态机。这时就得借助像symfony/workflow这样的组件。但在我做的这个商城里,状态是线性的,枚举已经把代码大大简化了。我也没打算为了炫技去引入重型依赖。
总结:这波升级值不值
当然值。代码里再也没有散落的魔法数字,IDE提示也友好,加状态只需要改一个枚举文件,不用全局搜索。前几天产品说“再加一个状态:已发货但用户未收到”,我加了一个ON_THE_WAY,然后在label()和actions()里各加一行,前端自动显示对应的按钮和文字。整个过程不到10分钟,比以前至少快了两个小时。
如果你还在用 PHP 7.x,真的可以考虑升级到 8.1 以上的版本,光是枚举这一个特性就值回票价。当然还有构造器属性提升、match表达式、readonly属性等,都是好东西。但今天我只想分享这个订单状态的实战,因为它帮我解决了一个真实业务痛点,也让我对PHP这个语言重新有了信心。

