戏里戏外

Laravel 中的枚举

2024-10-23#Laravel

枚举是在 PHP 8.1 中引入的,Laravel 充分利用了它的强大功能,这篇文章一起来看看如何在 Laravel 项目中使用枚举。

基础

在 Laravel 迁移中,可以定义一个可以保存多个预定义值的数据库列,列的值用于存储枚举的值。

<?php

enum PostStatus: string
{
    case DRAFT = 'draft';
    case PUBLISHED = 'published';
}

在示例中,此枚举类 PostStatus 为博客文章定义了两种状态,它的状态可以是draftpublished

下面是一个示例,说明如何设置迁移以根据这些值存储博客文章的状态:

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->string('status');
    $table->timestamps();
});

以下是将枚举值 draft 存储到 status 列上。

Post::create([
    'title' => 'A new post',
    'status' => PostStatus::DRAFT,
]);

枚举强制转换

在上面 Post 模型上的 PostStatus 枚举示例,当打印 status 列时会发生什么:

$post = Post::find(1);

dd($post->status); // 'draft'

假设将 status 字段的值存储为 draft,实际上会得到字符串 draft,这并不是很有帮助,如果充分利用了枚举,理想情况下希望将 PostStatus 枚举实例返回给我们。

所以在模型中需要这样设置:

class Post extends Model
{
    protected function cases(): array
    {
        return [
           'status' => PostStatus::class,    
        ];
    }
}

现在,当获取 status 列时,会返回 PostStatus 实例。

$post = Post::find(1);

dd($post->status);

//App\Enums\PostStatus {
//  +name: "DRAFT"
//  +value: "draft"
//}

这样可以快速轻松的根据代码中定义的状态进行判定

if ($post->status === PostStatus::DRAFT) {
    // Post is a draft
}

为了后续的功能也可以为每个枚举值扩展一个获取标签的 getLabel() 方法。

<?php

enum PostStatus: string
{
    case DRAFT = 'draft';
    case PUBLISHED = 'published';

    public function getLabel(): string {
        return match($this) {
            PostStatus::DRAFT => 'Draft',
            PostStatus::PUBLISHED => 'Published',
        };
    }
}

现在可以很容易的获得应该根据状态显示的标签:

$post = Post::find(1);

dd($post->status->getLabel()); // 'Draft'

枚举数组

如果模型可以包含多个状态,则还可以转换为一个枚举集合 Collection,比如 Post模型添加一个 review_flags字段用于存储包含各种审查标识。

Schema::table('posts', function (Blueprint $table) {
    $table->json('review_flags');
});

下面是一个新的枚举 PostReviewFlags 它包含所有可能的审查标识:

enum PostReviewFlags: string
{
    case OUTDATED = 'outdated';
    case SPELL_CHECK = 'spell_check';
}

在模型中可以这样转换 review_flags 字段:

class Post extends Model
{
    public function casts()
    {
        return [
            'status' => PostStatus::class,
           'review_flags' => AsEnumCollection::of(PostReviewFlags::class)
        ];
    }
}

一旦创建的文章过时了并且需要重新检查,现在可以像这样更新它:

$post = Post::find(1);

$post->update([
    'review_flags' => [
        PostReviewFlags::OUTDATED,
        PostReviewFlags::SPELL_CHECK,
    ]
]);

这将像这样将值存储在数据库中:

["outdated", "spell_check"]

由于使用的模型进行强制类型转换,因此尝试访问模型的 review_flags 属性时,会得到以下结果:

$post = Post::find(1);

dd($post->review_flags);
//Illuminate\Support\Collection {
//   #items: array:2 [
//    0 => App\Enums\PostReviewFlags {#1074 ▶}
//    1 => App\Enums\PostReviewFlags {#1004 ▶}
//  ]
//}

可以通过执行以下操作来检查帖子是否被标记为 outdated

if ($post->review_flags->contains(PostReviewFlags::OUTDATED)) {
    // It's outdated!
}

使用枚举验证数据

按照这个 Post 例子,如果管理界面允许为文章选择状态。可以这样编写验证规则:

use App\Enums\PostStatus;
use Illuminate\Validation\Rule;

Route::post('/posts', function (Request $request) {
    $request->validate([
        'status' => ['required', Rule::enum(PostStatus::class)]
    ]);
});

这可确保在表单中提交的 status 值包含枚举中定义的 draftpublished

也可以指定限制某些枚举值:

\Illuminate\Validation\Rule::enum(PostStatus::class)->only([PostStatus::DRAFT]); // 仅包含 draft

\Illuminate\Validation\Rule::enum(PostStatus::class)->except([PostStatus::PUBLISHED]); // 忽略 published

更多个复杂验证逻辑可以参考官方文档 Validation

隐式枚举绑定

与路由模型绑定一样,Laravel 也支持使用枚举值在路由中进行绑定。假设专门创建了一个路由来显示所有状态为 draft 帖子:

Route::get('/posts/{status}', function (PostStatus $status) {
    $posts = Post::where('status', $status->value)->get();

    dd($posts);
});

此时无论访问 /posts/published 还是 /posts/draft 这都将馈送到查询生成器中,并根据选择的状态返回帖子列表。

如果访问 /posts/nope,这将失败并返回 404。因为 nope 值不存在于 PostStatus 枚举类中。