Enumerations: We have time for them. Crafting clearer, more reliable code

Published on March 21st, 2024.

Questions

If you find yourself needing to categorize roles like 'admin', 'user', 'manager' or 'user non classified' within your user table in a Laravel application.
What approach would you take in your Laravel application? 🤔

  • Should you use a native PHP Enum?
  • Should you use a tool made for Laravel called spatie/laravel-enum?
  • Maybe you could make a relationship table to show the roles?
  • Or should you just create a column in you database and expects that everyone undestands?

Each of these approaches has its own pros and cons. In this particular case, I chose to use a Laravel Enum.

The goal

The goal of this article is to show you how to use Laravel Enum to create a more reliable and clear code. We will also see how to use it in migration, model, factory and how to test it.

What is an Enum?

"Enumerations are a restricting layer on top of classes and class constants, intended to provide a way to define a closed set of possible values for a type."

<?php

enum UserRole: string
{
    case 'ADM' = 'Admin',
    case 'USR' = 'User',
    case 'MNG' = 'Manager',
    case 'NON' = 'None',

    public function label(): string
    {
        return match($this) {
            static::Admin => 'Admin',
            static::User => 'User',
            static::Manager => 'Manager',
            static::None => 'User non classified',
        };
    }
}
?>

Why Laravel Enum?

Laravel Enum is a package that allows you to create and use enumerations in your Laravel application. It is a simple and powerful package that can help you to create more reliable and clear code. It is also allows you to use pollimorphic relationships instead of native PHP enums that you can't.
"Enums cannot be extended, and must not inherit"

Instalation

In this example I'll use a empty laravel application.
Then, install the package:

Inputs

You can use this custom inputs to create your own enum:

Enum

Use the following command to create a new enum: Past this code to the new file:

<?php

namespace App\Enums;
use Spatie\Enum\Laravel\Enum;

/**
 * The Status enum.
 *
 * @method static self ADM()
 * @method static self USR()
 * @method static self MNG()
 * @method static self NON()
 */
 class UserRoleEnum extends Enum
{
    const DEFAULT = 'NON';

    protected static function values(): array
    {
        return [
            'ADM' => 'Admin',
            'USR' => 'User',
            'MNG' => 'Manager',
            'NON' => 'User non classified',
        ];
    }
}

Migration

Let's create a new migration to add the role column to the users table: Past this code to the new file:

<?php

use App\Enums\UserRoleEnum;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->enum(role, UserRoleEnum::toArray())->default(UserRoleEnum::DEFAULT)->after('password');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn(role);
        });
    }
};
Run the migration:

Factory

<?php

namespace Database\Factories;

use App\Enums\UserRoleEnum; // ADD THIS
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
 */
class UserFactory extends Factory
{
    ...

    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            ...
            'role'' => fake()->randomElement(UserRoleEnum::toArray()),               // ADD THIS
        ];
    }

    ...
}

Model

I like to adjust the model to use the enum description. For example:
If the users role is 'ADM' the description will be 'Admin'. And I also adjust set method to ensure that will convert to database required enum.

<?php

namespace App\Models;

use App\Enums\UserRoleEnum; // ADD THIS


class User
{
   ...

    public function getRoleAttribute($value): string
    {
        return UserRoleEnum::from($value)->value;
    }

    public function setRoleAttribute($value): void
    {
        if (!empty($value))
            // check if value is a value or label
            if (in_array($value, UserRoleEnum::toValues()))
                $this->attributes[role] = UserRoleEnum::from($value)->label;
            else if (in_array($value, UserRoleEnum::toLabels()))
                $this->attributes[role] = $value;
            else
                throw new \InvalidArgumentException('Invalid User role value.');
        else
            $this->attributes[role] = UserRoleEnum::DEFAULT;
    }
}

Let's check with tinker

Lets open it and check if everything is working as expected:

$users = User::find(3);

[!] Aliasing 'User' to 'App\Models\User' for this Tinker session.
= App\Models\User {#5050
    id: 3,
    name: "Tamia Borer",
    email: "brenna13@example.org",
    email_verified_at: "2024-03-21 20:04:50",
    #password: "$2y$12$S5bDxjRM4E6nuXQzTX2BjeJKVWhvzi5sMe0UZiLs5PxB0fg7y.Y4O",
    #remember_token: "xyOyRzeuMV",
    created_at: "2024-03-21 20:04:50",
    updated_at: "2024-03-21 20:04:50",
    role: "NON",
  }

> $users->role
= "User non classified"

> App\Enums\UserRoleEnum::ADM()->label
= "ADM"

> App\Enums\UserRoleEnum::ADM()->value
= "Admin"

Testing

I will create a Test to ensure that the enum is working as expected.
Let's install Pest;

Now, create a test file

Past this code to the new file:

<?php

//create a new users and check if the users role is Ini

use App\Models\User;
use App\Enums\UserRoleEnum;

test('create users random role', function () {
    $users = User::factory()->create();
    expect($users->role)->toBeString();
})->skip('This test is skipped because the users role is random');

test('create users with role ADM', function () {
    $users = User::factory()->create([role => UserRoleEnum::ADM()->label]);
    expect($users->role)->toBe('Admin');
});

test('create users with role Admin', function () {
    $users = User::factory()->create([role => UserRoleEnum::ADM()->value]);
    expect($users->role)->toBe('Admin');
});

test('does not set an invalid role attribute', function () {
    $users = new User();
    $invalidRole = 'InvalidRole';

    // Act & Assert
    $this->expectException(\InvalidArgumentException::class);
    $this->expectExceptionMessage('Invalid users role value.');

    // Attempt to set an invalid role attribute
    $users->setroleAttribute($invalidRole);
});

Run the test:

list test

Conclusion

Enumerations are a great way to create more reliable and clear code. Laravel Enum is a powerful package that can help you to create enumerations in your Laravel application. It is simple to use and can help you to create more reliable and clear code. I hope this article has helped you to understand how to use Laravel Enum to create more reliable and clear code.