DEV Community

Cover image for Writing Flexible Enums in PHP with Interfaces and Traits
Nasrul Hazim Bin Mohamad
Nasrul Hazim Bin Mohamad

Posted on

Writing Flexible Enums in PHP with Interfaces and Traits

PHP enums are a powerful tool for defining a fixed set of constants while enabling you to attach functionality to those constants. In addition to simply holding values, enums can implement interfaces and use traits to expand their capabilities. This makes them more flexible and reusable in complex applications.

In this post, we will take your PHP enum design to the next level by combining enums with interfaces and traits. We’ll look at how you can create enums that provide labels, descriptions, and even generate options for select inputs in forms. By the end, you'll have a reusable structure that allows for easy expansion and consistent behaviour across all your enums.

Why Use Enums with Interfaces and Traits?

Enums are a natural fit for representing states, statuses, or types. However, simply having a list of constants is often not enough. In many scenarios, you might need:

  • Descriptive labels or human-readable names for each enum case.
  • Detailed descriptions explaining the meaning of each enum case.
  • Structured options for select inputs in user interfaces.

By implementing interfaces and using traits, you can ensure that your enums are consistent, extensible, and adaptable to evolving requirements.

Creating the Enum Interface

We’ll start by defining an interface that any enum can implement to ensure it has methods for returning labels, descriptions, and generating options for select inputs.

Here’s the interface:

<?php

namespace App\Contracts;

interface Enum
{
    /**
     * Get the label for the enum case.
     *
     * @return string
     */
    public function label(): string;

    /**
     * Get the description for the enum case.
     *
     * @return string
     */
    public function description(): string;

    /**
     * Generate an array of options with value, label, and description for select inputs.
     *
     * @return array
     */
    public static function options(): array;
}
Enter fullscreen mode Exit fullscreen mode

This interface defines three methods:

  1. label: Returns a human-readable name for each enum case.
  2. description: Provides a detailed description of each enum case.
  3. options: Generates an array of options, where each option contains the enum’s value, label, and description. This can be useful when building forms.

Leveraging Traits for Reusable Logic

We can further enhance our enums by using a trait to encapsulate common logic for generating options from enum cases. This is where the InteractsWithEnumOptions trait comes in.

<?php

namespace App\Concerns;

trait InteractsWithEnumOptions
{
    /**
     * Generate an array of options with value, label, and description for select inputs.
     *
     * @return array
     */
    public static function options(): array
    {
        return array_map(fn ($case) => [
            'value' => $case->value,
            'label' => $case->label(),
            'description' => $case->description(),
        ], self::cases());
    }
}
Enter fullscreen mode Exit fullscreen mode

This trait provides a reusable options() method, which loops through all enum cases (self::cases()) and returns an array where each item contains the enum case's value, label, and description. This is particularly helpful when generating select dropdown options.

Implementing the Interface and Trait in an Enum

Let’s now create an enum for managing server statuses. This enum will implement the Enum interface and use the InteractsWithEnumOptions trait to generate options for forms.

<?php

namespace App\Enums;

use App\Contracts\Enum as Contract;
use App\Concerns\InteractsWithEnumOptions;

enum ServerStatus: string implements Contract
{
    use InteractsWithEnumOptions;

    case PENDING = 'pending';
    case RUNNING = 'running';
    case STOPPED = 'stopped';
    case FAILED = 'failed';

    public function label(): string
    {
        return match ($this) {
            self::PENDING => 'Pending Installation',
            self::RUNNING => 'Running',
            self::STOPPED => 'Stopped',
            self::FAILED => 'Failed',
            default => throw new \Exception('Unknown enum value requested for the label'),
        };
    }

    public function description(): string
    {
        return match ($this) {
            self::PENDING => 'The server is currently being created or initialized.',
            self::RUNNING => 'The server is live and operational.',
            self::STOPPED => 'The server is stopped but can be restarted.',
            self::FAILED => 'The server encountered an error and is not running.',
            default => throw new \Exception('Unknown enum value requested for the description'),
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

What’s Happening Here?

  1. Enum Cases: We’ve defined several cases (e.g., PENDING, RUNNING, STOPPED, FAILED) that represent different server statuses.
  2. Label and Description Methods: The label() and description() methods provide human-readable names and detailed descriptions for each enum case. These methods use a match statement to map each case to its corresponding label and description.
  3. Trait for Options: The InteractsWithEnumOptions trait is used to automatically generate an array of options for form select inputs, containing the case’s value, label, and description.

Adding Flexibility with Reusable Stubs

If you’re working on multiple enums and want a quick way to set them up with similar functionality, you can use a reusable stub that combines everything we’ve covered. Here’s a refined version of such a stub:

<?php

namespace {{ namespace }};

use App\Contracts\Enum as Contract;
use App\Concerns\InteractsWithEnumOptions;

enum {{ class }} implements Contract
{
    use InteractsWithEnumOptions;

    case EXAMPLE; // Add actual cases here

    public function label(): string
    {
        return match ($this) {
            self::EXAMPLE => 'Example Label', // Add labels for other cases
            default => throw new \Exception('Unknown enum value requested for the label'),
        };
    }

    public function description(): string
    {
        return match ($this) {
            self::EXAMPLE => 'This is an example description.', // Add descriptions for other cases
            default => throw new \Exception('Unknown enum value requested for the description'),
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Customizing the Stub:

  • Replace {{ namespace }} with the actual namespace of your enum.
  • Add relevant enum cases inside the case block and define corresponding labels and descriptions.

Why This Pattern Works

  1. Consistency Across Enums: With this setup, every enum implementing the Enum interface must provide labels and descriptions. This ensures that all enums follow the same pattern, making your codebase more maintainable.
  2. Reusability with Traits: The InteractsWithEnumOptions trait encapsulates common logic for generating options, which can be reused across multiple enums without duplicating code.
  3. Scalability: As your application grows, adding new enum cases or additional methods becomes a seamless process. If you need to modify how the options are generated or add new functionality, you can do so centrally in the trait or interface.

Example Usage in a Laravel Blade Template

Let's add an example of how you can use the enum in a Laravel Blade template. Suppose we are working with the ServerStatus enum and we want to generate a select input for users to choose a server status, using the options generated by the InteractsWithEnumOptions trait.

First, ensure that your controller is passing the enum options to the view. For example:

Controller Example:

<?php

namespace App\Http\Controllers;

use App\Enums\ServerStatus;

class ServerController extends Controller
{
    public function edit($id)
    {
        $server = Server::findOrFail($id);
        $statusOptions = ServerStatus::options(); // Get enum options using the trait

        return view('servers.edit', compact('server', 'statusOptions'));
    }
}
Enter fullscreen mode Exit fullscreen mode

The ServerStatus::options() method will return an array with the structure:

[
    ['value' => 'pending', 'label' => 'Pending Installation', 'description' => 'The server is currently being created or initialized.'],
    ['value' => 'running', 'label' => 'Running', 'description' => 'The server is live and operational.'],
    // other cases...
]
Enter fullscreen mode Exit fullscreen mode

Blade Template Example:

In your Blade template, you can now use the statusOptions to populate a select dropdown and display a description when the user selects a status.

@extends('layouts.app')

@section('content')
    <div class="container">
        <h1>Edit Server</h1>

        <form action="{{ route('servers.update', $server->id) }}" method="POST">
            @csrf
            @method('PUT')

            <!-- Other form fields -->

            <div class="mb-3">
                <label for="status" class="form-label">Server Status</label>
                <select name="status" id="status" class="form-control" onchange="updateDescription()">
                    @foreach ($statusOptions as $option)
                        <option value="{{ $option['value'] }}" 
                                @if ($server->status === $option['value']) selected @endif>
                            {{ $option['label'] }}
                        </option>
                    @endforeach
                </select>
            </div>

            <div id="statusDescription" class="alert alert-info">
                <!-- Default description when the page loads -->
                {{ $statusOptions[array_search($server->status, array_column($statusOptions, 'value'))]['description'] }}
            </div>

            <button type="submit" class="btn btn-primary">Update Server</button>
        </form>
    </div>

    <script>
        const statusOptions = @json($statusOptions);

        function updateDescription() {
            const selectedStatus = document.getElementById('status').value;
            const selectedOption = statusOptions.find(option => option.value === selectedStatus);
            document.getElementById('statusDescription').textContent = selectedOption ? selectedOption.description : '';
        }
    </script>
@endsection
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. Controller: The ServerStatus::options() method is called in the controller to generate the array of options, which is then passed to the Blade view.

  2. Blade Template:

    • A <select> element is created to allow the user to choose a server status. The options are populated using the $statusOptions array.
    • The onchange event on the select triggers the updateDescription() JavaScript function, which updates the description below the dropdown based on the selected option.
    • Initially, the description of the current server status is displayed when the page loads.
  3. JavaScript: The statusOptions array is passed to JavaScript using the @json directive, and the updateDescription() function updates the description whenever the user changes the selected status.

This setup makes use of the enum options in a user-friendly way, leveraging both the label and description methods from the enum, providing a rich experience in your forms.

Conclusion

By combining PHP enums with interfaces and traits, you create a flexible, scalable, and reusable pattern for managing constants with additional logic like labels, descriptions, and select options. This design ensures consistency and maintainability across your codebase, even as the complexity of your application grows.

With the interface and trait structure we've built, you can now implement enums in a clean, organized manner. Whether you're dealing with server statuses, user roles, or payment states, this approach will help you write cleaner and more maintainable code.

Happy coding!


About the Author

Nasrul Hazim is a Solution Architect and Software Engineer with over 14 years of experience. You can explore more of his work on GitHub at nasrulhazim.

Top comments (0)