PHP 8.3 枚举与只读属性:构建类型安全的应用

2026-05-06 0 957

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环境中测试通过。

PHP 8.3 枚举与只读属性:构建类型安全的应用
收藏 (0) 打赏

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

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

淘吗网 php PHP 8.3 枚举与只读属性:构建类型安全的应用 https://www.taomawang.com/server/php/1772.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

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

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