从 PHP 8.1 原生枚举到 Laravel 中的类型安全转换,这一路径已成为现代 PHP 项目的必修课。直接切入正题:硬编码状态、类型、选项的原始字符串或数字,往往会让代码散落四处,逻辑含义模糊,稍有不慎就会成为线上 Bug 的温床。例如,把 'pending' 误写成 'pendding',或者在数据库里存入一个 2,却无人能记得它对应的是「已发货」还是「已取消」。这类问题一旦上线,排查成本远高于从一开始就做好约束。

接下来,我们先探讨必要性,再给出三种具体的落地方式——分别适用于不同版本和项目场景。
为什么必须用 Enum 替代魔法值
PHP 8.1 的原生枚举早已不是「新特性」,但即便是 Laravel 8 这样的项目,也完全可以通过 myclabs/php-enum 或自定义抽象基类来获得类型安全的好处。不使用枚举,类似 Order::status === 'shipped' 的判断既无法获得 IDE 智能提示,也无法在编译期拦截拼写错误。更棘手的是:一旦数据库字段的值需要变更,你必须翻遍所有 Controller、Model、Job 中散落的字符串,手动替换——漏掉一个就是线上事故。
这一步不是锦上添花,而是堵住可预见的维护漏洞。
方法一:使用 myclabs/php-enum 库(兼容 PHP 7.4+)
如果你的项目仍在 PHP 7.4 到 8.0 之间,这个库是一个稳妥的过渡方案。
首先安装依赖:
composer require myclabs/php-enum
创建 app/Enums/OrderStatus.php,继承 MyCLabsEnumEnum:
php
namespace AppEnums;
use MyCLabsEnumEnum;
/**
* 订单状态枚举
* @method static OrderStatus PENDING()
* @method static OrderStatus CONFIRMED()
* @method static OrderStatus SHIPPED()
* @method static OrderStatus CANCELLED()
*/
final class OrderStatus extends Enum
{
private const PENDING = 'pending';
private const CONFIRMED = 'confirmed';
private const SHIPPED = 'shipped';
private const CANCELLED = 'cancelled';
}
然后在模型中直接使用:$order->status = OrderStatus::SHIPPED();,赋值时自动校验合法性。需要获取底层字符串值就调用 $order->status->getValue(),获取常量名就调用 $order->status->getKey()。
【注意】这个库不支持 PHP 8.1+ 原生枚举的语法糖,而且 getValue() 返回的是字符串。如果数据库字段是 tinyint 类型,你需要额外做一层映射转换,否则会报类型不匹配的错误。
方法二:手写抽象基类 + PHP 8.1+ 原生 Enum(推荐新项目)
如果项目已升级到 PHP 8.1,原生枚举显然更轻量、IDE 支持更好,而且无需引入第三方依赖。
创建 app/Enums/OrderStatus.php:
php
namespace AppEnums;
enum OrderStatus: string
{
case PENDING = 'pending';
case CONFIRMED = 'confirmed';
case SHIPPED = 'shipped';
case CANCELLED = 'cancelled';
public function label(): string
{
return match($this) {
self::PENDING => '待确认',
self::CONFIRMED => '已确认',
self::SHIPPED => '已发货',
self::CANCELLED => '已取消',
};
}
}
在控制器中判断状态:if ($order->status === OrderStatus::SHIPPED) —— IDE 会自动补全、类型推导精准,PHPStan 静态分析也能捕获非法赋值。
数据库写入时用 $order->status->value,读取时用 OrderStatus::from($dbValue)。如果传入的值不在枚举中,from() 会抛出 ValueError,天然就是一道强约束。
方法三:Laravel 专属封装 —— 使用 Casts 处理枚举字段
让 Eloquent 模型自动完成枚举转换,可以省去每次手动调用 from() 和 ->value 的麻烦。
创建 app/Casts/OrderStatusCast.php:
php
namespace AppCasts;
use AppEnumsOrderStatus;
use IlluminateContractsDatabaseEloquentCastsAttributes;
class OrderStatusCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return OrderStatus::tryFrom($value) ?? OrderStatus::PENDING;
}
public function set($model, $key, $value, $attributes)
{
return $value instanceof OrderStatus ? $value->value : $value;
}
}
然后在 app/Models/Order.php 中声明:
protected $casts = [
'status' => OrderStatusCast::class,
];
之后对 $order->status 的读写就完全透明了:赋值时写 $order->status = OrderStatus::SHIPPED,读取时调用 $order->status->label()。数据库里存的是字符串 'shipped',而代码里操作的是类型安全的枚举实例。
【关键前提】必须确保数据库字段类型为 varchar 或 enum,不能是 int。如果历史字段是 tinyint,需要先迁移为字符串类型再启用这个 Cast,否则 tryFrom() 会因为类型不匹配而静默失败。
