2025年,PHP 8.3的枚举(Enum)和只读属性(Readonly)已经成为现代PHP开发的核心特性。枚举让状态管理变得类型安全,只读属性则让数据不可变性得以保障。本文通过四个实战案例,带你掌握这些现代PHP特性。
1. 为什么需要枚举与只读属性?
传统PHP使用常量或字符串表示状态,容易出错且缺乏类型安全。枚举提供了真正的类型安全枚举值,只读属性则防止数据被意外修改,让代码更加健壮和可预测。
- 枚举:类型安全的枚举值,可包含方法和接口
- 只读属性:构造函数初始化后不可修改
- 只读类:类的所有属性自动变为只读
2. 枚举基础:定义与使用
PHP 8.1+ 支持枚举,8.3 增强了枚举能力。
<?php
// 基础枚举
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
// 枚举方法
public function label(): string
{
return match($this) {
self::Pending => '待支付',
self::Paid => '已支付',
self::Shipped => '已发货',
self::Delivered => '已送达',
self::Cancelled => '已取消',
};
}
// 判断是否可取消
public function canCancel(): bool
{
return match($this) {
self::Pending, self::Paid => true,
default => false,
};
}
}
// 使用枚举
$status = OrderStatus::Paid;
echo $status->value; // paid
echo $status->label(); // 已支付
echo $status->canCancel() ? '可取消' : '不可取消'; // 可取消
// 从值创建枚举
$statusFromValue = OrderStatus::from('shipped');
echo $statusFromValue->label(); // 已发货
3. 实战案例一:订单状态机
使用枚举实现类型安全的订单状态机。
<?php
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
// 定义允许的状态转换
public function allowedTransitions(): array
{
return match($this) {
self::Pending => [self::Paid, self::Cancelled],
self::Paid => [self::Shipped, self::Cancelled],
self::Shipped => [self::Delivered],
self::Delivered => [],
self::Cancelled => [],
};
}
public function canTransitionTo(self $newStatus): bool
{
return in_array($newStatus, $this->allowedTransitions(), true);
}
}
class Order
{
public function __construct(
public int $id,
private OrderStatus $status = OrderStatus::Pending
) {}
public function getStatus(): OrderStatus
{
return $this->status;
}
public function transitionTo(OrderStatus $newStatus): void
{
if (!$this->status->canTransitionTo($newStatus)) {
throw new InvalidArgumentException(
"不能从 {$this->status->value} 转换到 {$newStatus->value}"
);
}
$this->status = $newStatus;
echo "订单 #{$this->id} 状态已更新: {$newStatus->label()}n";
}
}
// 使用示例
$order = new Order(1001);
echo "当前状态: " . $order->getStatus()->label() . "n"; // 待支付
$order->transitionTo(OrderStatus::Paid); // 已支付
$order->transitionTo(OrderStatus::Shipped); // 已发货
$order->transitionTo(OrderStatus::Delivered); // 已送达
// 尝试非法转换
try {
$order->transitionTo(OrderStatus::Cancelled); // 不允许
} catch (InvalidArgumentException $e) {
echo "错误: " . $e->getMessage() . "n";
}
4. 实战案例二:只读属性与数据传输对象
使用只读属性构建不可变的数据传输对象(DTO)。
<?php
// PHP 8.2+ 支持只读类
readonly class UserDTO
{
public function __construct(
public int $id,
public string $name,
public string $email,
public ?string $phone = null,
public array $roles = ['user']
) {}
// 只读类中所有属性自动只读,方法可以存在
public function toArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'phone' => $this->phone,
'roles' => $this->roles,
];
}
}
// PHP 8.3 允许在非只读类中使用只读属性
class OrderDTO
{
public readonly int $id;
public readonly string $orderNo;
public readonly float $amount;
public readonly OrderStatus $status;
public function __construct(
int $id,
string $orderNo,
float $amount,
OrderStatus $status
) {
$this->id = $id;
$this->orderNo = $orderNo;
$this->amount = $amount;
$this->status = $status;
}
}
// 使用示例
$user = new UserDTO(
id: 1,
name: '张三',
email: 'zhangsan@example.com',
phone: '13800138000',
roles: ['admin', 'editor']
);
// $user->name = '李四'; // 错误:只读属性不可修改
echo "用户: {$user->name}, 邮箱: {$user->email}n";
print_r($user->toArray());
$order = new OrderDTO(1001, 'ORD-2025-001', 299.99, OrderStatus::Paid);
echo "订单: {$order->orderNo}, 金额: {$order->amount}n";
5. 实战案例三:枚举实现策略模式
使用枚举方法和接口实现策略模式。
<?php
// 支付方式枚举,实现策略模式
enum PaymentMethod: string
{
case Alipay = 'alipay';
case Wechat = 'wechat';
case CreditCard = 'credit_card';
// 每个枚举值可以有不同的行为
public function processPayment(float $amount): array
{
return match($this) {
self::Alipay => $this->processAlipay($amount),
self::Wechat => $this->processWechat($amount),
self::CreditCard => $this->processCreditCard($amount),
};
}
private function processAlipay(float $amount): array
{
// 模拟支付宝支付逻辑
return [
'method' => '支付宝',
'amount' => $amount,
'fee' => $amount * 0.006,
'transaction_id' => 'ALI' . uniqid(),
'status' => 'success',
];
}
private function processWechat(float $amount): array
{
// 模拟微信支付逻辑
return [
'method' => '微信支付',
'amount' => $amount,
'fee' => $amount * 0.006,
'transaction_id' => 'WX' . uniqid(),
'status' => 'success',
];
}
private function processCreditCard(float $amount): array
{
// 模拟信用卡支付逻辑
return [
'method' => '信用卡',
'amount' => $amount,
'fee' => $amount * 0.01,
'transaction_id' => 'CC' . uniqid(),
'status' => 'success',
];
}
}
// 使用枚举策略
function checkout(float $amount, PaymentMethod $method): void
{
echo "使用 {$method->value} 支付 {$amount} 元...n";
$result = $method->processPayment($amount);
echo "支付成功! 交易ID: {$result['transaction_id']}, 手续费: {$result['fee']}元n";
}
checkout(299.99, PaymentMethod::Alipay);
checkout(150.00, PaymentMethod::Wechat);
checkout(899.99, PaymentMethod::CreditCard);
6. 实战案例四:枚举与数据库交互
在Eloquent模型中使用枚举,实现类型安全的数据库操作。
<?php
// 定义文章状态枚举
enum ArticleStatus: string
{
case Draft = 'draft';
case Published = 'published';
case Archived = 'archived';
public function label(): string
{
return match($this) {
self::Draft => '草稿',
self::Published => '已发布',
self::Archived => '已归档',
};
}
}
// Eloquent模型中使用枚举
namespace AppModels;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentCastsAttribute;
class Article extends Model
{
// 自动转换枚举
protected $casts = [
'status' => ArticleStatus::class,
'published_at' => 'datetime',
];
protected $fillable = [
'title', 'content', 'status', 'author_id'
];
// 访问器
protected function statusLabel(): Attribute
{
return Attribute::get(fn() => $this->status->label());
}
// 查询作用域
public function scopePublished($query)
{
return $query->where('status', ArticleStatus::Published);
}
public function scopeDraft($query)
{
return $query->where('status', ArticleStatus::Draft);
}
}
// 使用示例
$article = Article::create([
'title' => 'PHP 8.3 新特性指南',
'content' => '枚举和只读属性...',
'status' => ArticleStatus::Draft,
'author_id' => 1,
]);
echo "文章状态: " . $article->status->label() . "n"; // 草稿
echo "状态值: " . $article->status->value . "n"; // draft
// 发布文章
$article->update(['status' => ArticleStatus::Published]);
echo "新状态: " . $article->status->label() . "n"; // 已发布
// 查询已发布的文章
$publishedArticles = Article::published()->get();
echo "已发布文章数: " . $publishedArticles->count() . "n";
7. 性能对比:传统方式 vs 枚举方式
| 指标 | 传统方式(常量+字符串) | 枚举方式 |
|---|---|---|
| 类型安全 | 无,任何字符串都可传入 | 强类型,编译器检查 |
| IDE支持 | 有限,无自动补全 | 完美支持自动补全和重构 |
| 可维护性 | 散落在代码各处的字符串 | 集中定义,易于修改 |
| 业务逻辑封装 | 需要额外的辅助函数 | 枚举自身可包含方法 |
8. 最佳实践总结
- 使用枚举替代常量:当状态或选项有固定集合时使用枚举
- 枚举可包含方法:将相关逻辑封装在枚举内部
- 只读类用于DTO:数据传输对象应该是不可变的
- 枚举实现策略模式:利用match表达式实现不同行为
- 数据库中使用枚举:结合Eloquent的casts自动转换
// 最佳实践:枚举与match表达式结合
enum UserRole: string
{
case Admin = 'admin';
case Editor = 'editor';
case Viewer = 'viewer';
public function permissions(): array
{
return match($this) {
self::Admin => ['create', 'read', 'update', 'delete', 'manage'],
self::Editor => ['create', 'read', 'update'],
self::Viewer => ['read'],
};
}
}
function checkPermission(UserRole $role, string $permission): bool
{
return in_array($permission, $role->permissions(), true);
}
$role = UserRole::Editor;
echo checkPermission($role, 'create') ? '允许' : '拒绝'; // 允许
echo checkPermission($role, 'delete') ? '允许' : '拒绝'; // 拒绝
9. 总结
通过本文的案例,你掌握了PHP 8.3枚举和只读属性的核心技术:
- 枚举的定义、方法和接口实现
- 枚举实现订单状态机
- 只读属性与数据传输对象
- 枚举实现策略模式
- 枚举与数据库交互
- 最佳实践与性能对比
PHP 8.3的枚举和只读属性让代码更加类型安全、可维护和健壮。现在就开始在你的项目中使用这些现代PHP特性吧!
本文原创,基于PHP 8.3+。所有代码均在PHP 8.3环境中测试通过。

