PHP 8.1 Enum实战:我把订单状态判断从80行减少到10行

2026-08-21 0 553

之前维护一个老商城项目,里面订单状态的代码写得那叫一个“壮观”:各种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这个语言重新有了信心。

PHP 8.1 Enum实战:我把订单状态判断从80行减少到10行
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 php PHP 8.1 Enum实战:我把订单状态判断从80行减少到10行 https://www.taomawang.com/server/php/2581.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务