บทความ

PHP Enum ใช้ยังไง ทั้ง Pure Enum, Backed Enum และใน Laravel

Enum รวมค่าคงที่กลุ่มเดียวกันไว้เป็น type เดียว มีทั้ง pure และ backed enum ใส่ method กับ interface ได้ และ Laravel รับ enum ได้ทั้งใน Eloquent cast, validation และ route

เผยแพร่
อัปเดต
— แก้ตัวอย่างโค้ดให้ถูกต้อง และเพิ่มวิธีใช้ enum ใน Laravel 13
เวลาอ่าน
5 นาที
แปลจาก
Brent Roose · stitcher.io
แท็ก
#Laravel#PHP

Enum มากับ PHP 8.1 ตั้งแต่ปลายปี 2021 ใช้รวมค่าคงที่ที่เป็นกลุ่มเดียวกัน เช่นสถานะของโพสต์ ไว้เป็น type เดียว แล้วเอา type นั้นไป type-hint ได้ ตอนนี้ PHP 8.1 หมดระยะ security support ไปแล้ว แต่ทุกอย่างในบทความนี้ใช้ได้กับ PHP เวอร์ชันที่ยังซัพพอร์ตอยู่ ตัวอย่างโค้ดลองรันกับ PHP 8.4 และ Laravel 13 แล้ว

ประกาศ enum แบบง่ายที่สุด

enum Status
{
    case Draft;
    case Published;
    case Archived;
}

แต่ละ case คือค่าหนึ่งของ Status เราเอา Status ไป type-hint ได้เหมือนคลาส

class Post
{
    public function __construct(
        public Status $status,
    ) {}
}

$post = new Post(Status::Draft);

ถ้าส่งค่าอื่นเข้าไป เช่น string 'draft' PHP จะโยน TypeError ทันที ต่างจาก constant ในคลาสแบบเดิมที่สุดท้ายก็เป็นแค่ string ส่ง string อะไรเข้าไปก็ผ่าน

enum ที่ไม่มีค่าผูกอยู่กับแต่ละ case แบบนี้ เราเรียกว่า pure enum

ใส่ method ใน enum ได้เหมือนคลาส

ตัวอย่างที่เจอบ่อยคือเอาสถานะไปแสดงเป็น label ที่มีสีและข้อความตามสถานะ เราเขียน method ไว้ใน enum ได้เลย แล้วใช้คู่กับ match ที่มากับ PHP 8 (เรื่อง match อ่านต่อได้ใน Syntax ใหม่ใน PHP 8)

enum Status
{
    case Draft;
    case Published;
    case Archived;
    case Trashed;

    public function color(): string
    {
        return match ($this) {
            self::Draft => 'gray',
            self::Published => 'green',
            self::Archived, self::Trashed => 'red',
        };
    }

    public function title(): string
    {
        return match ($this) {
            self::Draft => 'ร่าง',
            self::Published => 'เผยแพร่',
            self::Archived => 'เก็บถาวร',
            self::Trashed => 'ถังขยะ',
        };
    }
}

ใน enum เราใช้ self แทนชื่อ enum ได้ และ $this คือ case ที่เรียก method อยู่ เรียกใช้แบบนี้

$status = Status::Archived;

$status->color(); // 'red'
$status->title(); // 'เก็บถาวร'

enum มี static method ได้ด้วย เช่น method ที่คืนสถานะเริ่มต้นของโพสต์ใหม่

enum Status
{
    // ...

    public static function initial(): self
    {
        return self::Draft;
    }
}

Status::initial(); // Status::Draft

implement interface ได้เหมือนคลาส

interface HasColor
{
    public function color(): string;
}

enum Status implements HasColor
{
    case Draft;
    case Published;
    case Archived;
    case Trashed;

    public function color(): string { /* ... */ }
}

ทีนี้ function ที่รับ HasColor ก็รับ Status ได้ด้วย

ผูกค่าให้แต่ละ case ด้วย backed enum

ใน PHP แต่ละ case เป็น object ไม่ใช่ string หรือ int ถ้าจะเก็บลง database หรือส่งออกไปเป็น JSON เราต้องมีค่าที่เก็บได้ ให้ระบุ type ต่อท้ายชื่อ enum แล้วใส่ค่าให้ทุก case

enum Status: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';
    case Trashed = 'trashed';
}

: string บอกว่าค่าของทุก case เป็น string ถ้าอยากเก็บเป็นตัวเลขก็ใช้ : int

enum Status: int
{
    case Draft = 1;
    case Published = 2;
    case Archived = 3;
    case Trashed = 4;
}

enum ที่มีค่าแบบนี้เรียกว่า backed enum ส่วน enum ที่ไม่มีค่าแบบตัวอย่างแรกคือ pure enum ใน enum เดียวกันจะผสมสองแบบไม่ได้

ถ้าจะ implement interface ด้วย ให้ใส่ type ก่อนคำว่า implements

enum Status: string implements HasColor
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';
    case Trashed = 'trashed';

    // ...
}

แปลง case เป็นค่า และแปลงค่ากลับเป็น case

ทุก case มี property name เป็นชื่อ case และ backed enum มี value เป็นค่าของ case ทั้งสองตัวเป็น readonly

Status::Published->value; // 'published'
Status::Published->name;  // 'Published'

ขากลับ เราใช้ from() เปลี่ยนค่าเป็น case

Status::from('published'); // Status::Published

ถ้าค่าไม่ตรงกับ case ไหนเลย from() จะโยน ValueError ส่วน tryFrom() คืน null ให้เราจัดการเอง

Status::from('unknown');    // ValueError: "unknown" is not a valid backing value for enum Status
Status::tryFrom('unknown'); // null

ค่าที่มาจากผู้ใช้ เช่น query string ใช้ tryFrom() แล้วต่อด้วยค่า default ได้

$status = Status::tryFrom($_GET['status'] ?? '') ?? Status::Draft;

from() กับ tryFrom() มีเฉพาะ backed enum ถ้าอยากได้ case จากชื่อ ใช้ constant(Status::class.'::'.$name) หรือ Status::{$name} ที่เขียนได้ตั้งแต่ PHP 8.3

json_encode ใช้กับ backed enum ได้เลย

json_encode() แปลง backed enum เป็นค่าของ case ให้

json_encode(['status' => Status::Published]); // {"status":"published"}

pure enum ไม่มีค่าให้แปลง json_encode() จึงคืน false (หรือโยน JsonException ถ้าใส่ flag JSON_THROW_ON_ERROR) ถ้าอยากกำหนดเองว่าจะออกมาเป็นอะไร ให้ implement JsonSerializable ได้ทั้ง pure และ backed enum

serialize() กับ unserialize() ใช้กับ enum ได้ทุกแบบ และได้ case เดิมกลับมา

ดึง case ทั้งหมดด้วย cases()

cases() คืน array ของทุก case ตามลำดับที่ประกาศ เป็น list ธรรมดาที่ key เป็น 0, 1, 2 … ทั้ง pure และ backed enum

Status::cases();
// [Status::Draft, Status::Published, Status::Archived, Status::Trashed]

เอาไปทำตัวเลือกใน dropdown ได้เลย

array_map(fn (Status $status) => $status->title(), Status::cases());
// ['ร่าง', 'เผยแพร่', 'เก็บถาวร', 'ถังขยะ']

ถ้าอยากได้ array ที่ key เป็นค่าของ case ต้องสร้างเอง เช่นด้วย array_column()

array_column(Status::cases(), 'name', 'value');
// ['draft' => 'Draft', 'published' => 'Published', ...]

แต่ละ case เป็น object ตัวเดียวทั้งแอป

case ของ enum เป็น object ที่ PHP สร้างไว้ตัวเดียว (singleton) เรียก Status::Published กี่ครั้งก็ได้ object ตัวเดิม เราจึงเทียบด้วย === ได้

$a = Status::Published;
$b = Status::Published;

$a === $b;               // true
$a === Status::Archived; // false
$a instanceof Status;    // true

enum สั่ง new Status() ไม่ได้ และประกาศ property เองไม่ได้ มีแค่ name กับ value

และเพราะเป็น object เราจึงใช้ enum เป็น key ของ array ไม่ได้ ([Status::Draft => 1] โยน TypeError) ใช้ ->value เป็น key แทน หรือเก็บใน WeakMap ก็ได้

ใช้ trait ใน enum ได้ ถ้า trait ไม่มี property

enum ใช้ trait ได้เหมือนคลาส แต่ trait ต้องมีแค่ method ถ้า trait มี property PHP จะแจ้ง fatal error Enum Status cannot include properties

trait HasLabel
{
    public function label(): string
    {
        return ucfirst($this->value);
    }
}

enum Status: string implements HasColor
{
    use HasLabel;

    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';
    case Trashed = 'trashed';

    // ...
}

Status::Draft->label(); // 'Draft'

ประกาศ constant ใน enum และใช้ค่าของ case ใน constant

enum มี constant ของตัวเองได้ และ constant ชี้ไปที่ case ได้

enum Status: string
{
    case Draft = 'draft';
    case Published = 'published';

    const Default = self::Draft;
}

Status::Default; // Status::Draft

ตั้งแต่ PHP 8.2 เราอ่าน ->value หรือ ->name ของ case ใน constant expression ได้ด้วย เช่นใน constant ของคลาส ค่า default ของ parameter หรือ argument ของ attribute (RFC) ส่วนตัว case เองใช้เป็นค่า constant หรือค่า default ได้มาตั้งแต่ 8.1

class Post
{
    const DEFAULT_STATUS = Status::Draft->value; // 'draft' ต้องใช้ PHP 8.2 ขึ้นไป

    public function __construct(
        public Status $status = Status::Draft, // ได้ตั้งแต่ PHP 8.1
    ) {}
}

ใช้ enum ใน Laravel

Laravel รับ enum ได้หลายจุด จุดที่ใช้บ่อยมีสามที่ คือ Eloquent cast, validation และ route ทั้งสามแบบทำงานผ่าน value ของ case จึงควรใช้ backed enum

cast คอลัมน์ใน model ให้เป็น enum

วาง enum ไว้ใน app/Enums แล้วบอก model ใน casts() ว่าคอลัมน์ไหนใช้ enum ตัวไหน

namespace App\Enums;

enum Status: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';
    case Trashed = 'trashed';
}

พออ่าน status จาก model เราจะได้ case ของ enum แทน string และตอนบันทึก Laravel เก็บ value ลงคอลัมน์ให้

$post = Post::findOrFail(1);

$post->status;          // Status::Published
$post->status->title(); // 'เผยแพร่'

$post->status = Status::Archived;
$post->save();          // คอลัมน์ status เก็บ 'archived'

ตอนอ่าน Laravel เรียก Status::from() กับค่าในคอลัมน์ ถ้าใน database มีค่าที่ไม่มีใน enum เราจะเจอ ValueError ส่วนตอนเขียน ถ้ากำหนดเป็น string ตรง ๆ ($post->status = 'archived') Laravel ก็แปลงผ่าน from() ให้เหมือนกัน

ใน query เราส่ง enum เข้าไปได้เลย Laravel แปลงเป็นค่าให้เอง

Post::where('status', Status::Published)->get();

pure enum ก็ cast ได้ แต่ Laravel จะเก็บชื่อ case ('Published') ลงคอลัมน์แทน วันไหนเปลี่ยนชื่อ case ข้อมูลเดิมก็อ่านไม่ได้ ถ้าคอลัมน์ JSON เก็บหลายค่า ใช้ AsEnumCollection::of(Status::class) ได้ อ่านเพิ่มใน Enum Casting

validate ค่าที่ผู้ใช้ส่งมาด้วย Rule::enum()

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

public function rules(): array
{
    return [
        'status' => ['required', Rule::enum(Status::class)],
    ];
}

Rule::enum() เช็คด้วย tryFrom() ว่าค่าตรงกับ case ไหนสักตัว จึงใช้ได้กับ backed enum เท่านั้น ถ้าอยากรับแค่บาง case ต่อด้วย only() หรือ except()

'status' => ['required', Rule::enum(Status::class)->except(Status::Trashed)],

ข้อความ error เริ่มต้นคือ "The selected status is invalid." (rule enum) เรื่อง Form Request กับการเปลี่ยนข้อความ error อ่านต่อได้ใน Laravel Validation ทำได้กี่แบบ

ใน controller ดึงค่าออกมาเป็น enum ได้เลยด้วย $request->enum() ถ้าค่าว่างหรือไม่ตรงกับ case ไหน จะได้ null

$status = $request->enum('status', Status::class);

รับ enum จาก URL ด้วย route binding

type-hint backed enum ใน route แล้ว Laravel แปลงค่าใน URL เป็น case ให้ (Implicit Enum Binding)

use App\Enums\Status;
use App\Models\Post;
use Illuminate\Support\Facades\Route;

Route::get('/posts/{status}', function (Status $status) {
    return Post::where('status', $status)->get();
});

เปิด /posts/published จะได้ Status::Published ส่วน /posts/unknown ได้ 404 ทันที เพราะ Laravel หา case ด้วย tryFrom() ไม่เจอ เราไม่ต้องเช็คเอง

เอกสาร enum ฉบับเต็ม

รายละเอียดทั้งหมดอยู่ใน คู่มือ PHP เรื่อง Enumerations ถ้าอยากรู้ว่าทำไมออกแบบมาแบบนี้ ลองอ่าน RFC: Enumerations ต่อได้

อ่านต่อ

ล่าสุด

ดูทั้งหมด →