Enums in Laravel: Everything You Need To Know

October 8th, 2024 • 7 minutes read time

Enums were introduced in PHP 8.1, and Laravel has taken full advantage of their power. Let's take a look at how you're able to use Enums in your Laravel projects today.

When we talk about PHP Enums, it's important not to confuse this with the enum database type. In your Laravel migrations, you're able to use the enum method to define a database column that can hold multiple pre-defined values — that's not what we're covering here.

Instead, here's what a PHP Enum looks like:

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

In this example, this Enum defines two statuses for a blog post. It's status can either be a draft, or published.

This is a backed enum, meaning each case has a value (in this case, a string). This makes it super easy to store in a database column as either draft or published.

Here's an example of how we could set up a migration to store the status of a blog post based on these values:

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

And here's how we'd store a Post with the draft status using our Enum:

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

This would insert the string draft into the status column of the database.

Great, we now have a pre-defined set of statuses written into our code that we can use to insert, compare and display the status of a Post. There's more to Enums than this, so check out our PHP Enums course if you still need some pointers on the basics.

If you understand the basics of Enums, let's dive into how we can use them in Laravel!

Continuing with our example of a PostStatus Enum on a Post model, let's see what happens when we dump out the status column:

$post = Post::find(1);
dd($post->status); // 'draft'

Assuming we stored this as a draft, we literally get the string draft. That's not super helpful. If we're making full use of enums, ideally, we'd like a PostStatus enum instance returned to us.

Model casting in Laravel isn't new, but more recently, Laravel has allowed you to cast using an Enum. Here's what it looks like on our Post model:

class Post extends Model
{
    public $casts = [
        'status' => PostStatus::class,
    ];
}

Now, when we grab the status column, we get the Enum instance returned, mapped up to the draft type:

$post = Post::find(1);
dd($post->status);

// This now returns:
App\Enums\PostStatus {#251 ▼ // routes/web.php:10
  +name: "DRAFT"
  +value: "draft"
}

This means we can quickly and easily check the status against the Enum defined in our code like this:

$post = Post::find(1);

if ($post->status === PostStatus::DRAFT) {
    // It's a draft!
}

You might be wondering, how is this better than comparing strings?. Well, if our DRAFT case value changes to draft_status at any point in the future, there's not much to update. Like this:

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

In this case, the if statement before will work in exactly the same way (assuming we update the database and change draft to draft_status):

if ($post->status === PostStatus::DRAFT) {
    // This still works, since we're not referencing a `draft` string.
}

The takeaway from Enums and casting is this: If you have a column in your database that can have multiple values, create an Enum, cast it, and use the Enum to compare values. If anything changes in the future, you'll thank yourself for having a backed Enum defined in code without having to compare strings in multiple places throughout your application.

A great case for using Enums and applying a cast in models is that you're able to get a label for each Enum value. This means you can instantly grab a UI value for each PostStatus case to display.

Let's update the Enum we're working with to demonstrate this:

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

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

Now, combining this with our case, we can easily get the label that should be shown based on the status:

$post = Post::find(1);
dd($post->status->label()); // 'Draft'

Of course, if you were doing this in a Blade template, it's now super easy to display the status of the post without having to create a bunch of IF statements or fiddle around with the formatting of the column value:

Post status: {{ $post->status->label() }}

If you have a model that can contain multiple statuses, you're also able to cast as a Collection of Enums.

Imagine our Post could contain various review_flags, like whether it needs to be checked for outdated information or spelling. Here's another migration and Enum that could house that:

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

And here's a new Enum, PostReviewFlags, that contains all the possible review flags:

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

Because we're using a json column type that could hold an array of these flags, we can cast like this:

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

Note that we're now using a method to return our casts. Using AsEnumCollection::of in a class property isn't valid PHP.

If a post we'd created was unfortunate enough to be outdated and needed a spell check, we could now update it like this:

$post = Post::find(1);

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

This would store the value in the database like this:

["outdated", "spell_check"]

And since we're casting, here's what we get back when we try to access the review_flags property of our model:

$post = Post::find(1);
dd($post->review_flags);

// We now get a Collection of Enum instances back!
Illuminate\Support\Collection {#955 ▼ // routes/web.php:11
  #items: array:2 [▼
    0 => App\Enums\PostReviewFlags {#1074 ▶}
    1 => App\Enums\PostReviewFlags {#1004 ▶}
  ]
  #escapeWhenCastingToString: false
}

So, as an example, we could check if a post was flagged as outdated by doing something like this:

$post = Post::find(1);

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

You could also iterate over the returned collection to show each review flag with a label (as we looked at earlier).

Keeping with the Post example, imagine our admin interface allowed a status to be chosen for the article. Of course, we'd want to validate this.

Here's how we validate an Enum in Laravel, using a simple route as an example:

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

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

This ensures the value submitted in the status of the form contains either draft or published as per our Enum. It works like any other validation rule, so you're able to capture the validation message and display it on your forms.

You're also able to limit which Enum values are accepted:

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

There are a couple of other things you can do with Enum validation, depending on how complex the logic is.

Like route model binding, Laravel also supports binding in routes using Enum values. Keeping with our Post example, let's imagine we created a route specifically to show all posts in draft status:

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

    dd($posts);
});

This works, but we can make this more dynamic by creating a route that responds to any PostStatus type... directly from our Enum values!

Here's how that would look:

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

    dd($posts);
});

Based on our example, whether we hit /posts/draft or /posts/published, this will feed into the query builder and return a list of posts based on the status we've chosen.

Why is this helpful? Well, the route binding will only respond where the Enum value actually exists. So, if we were to access /posts/nope, this would fail with a 404 status code. It's exactly like route model binding but for Enums.

Enums provide a powerful way to keep track of different model states in your applications by defining cases through code, giving you more safety than relying on storing, comparing and outputting basic scalar values like strings.

And, luckily for us, Laravel provides a few ways to integrate Enums into models, validation and routing, helping to keep our code even cleaner.

If you'd like to learn more, you can check out our PHP Enums course or Enums in Laravel course to learn more.

If you found this article helpful, you'll love our practical screencasts.
Author
Alex Garrett-Smith
Share :

Comments

No comments, yet. Be the first!