Nasrul Hazim Bin Mohamad
Posted on October 10, 2024
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;
}
This interface defines three methods:
- label: Returns a human-readable name for each enum case.
- description: Provides a detailed description of each enum case.
- 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());
}
}
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'),
};
}
}
What’s Happening Here?
-
Enum Cases: We’ve defined several cases (e.g.,
PENDING
,RUNNING
,STOPPED
,FAILED
) that represent different server statuses. -
Label and Description Methods: The
label()
anddescription()
methods provide human-readable names and detailed descriptions for each enum case. These methods use amatch
statement to map each case to its corresponding label and description. -
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'),
};
}
}
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
-
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. -
Reusability with Traits: The
InteractsWithEnumOptions
trait encapsulates common logic for generating options, which can be reused across multiple enums without duplicating code. - 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'));
}
}
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...
]
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
Explanation:
Controller: The
ServerStatus::options()
method is called in the controller to generate the array of options, which is then passed to the Blade view.-
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 theupdateDescription()
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.
- A
JavaScript: The
statusOptions
array is passed to JavaScript using the@json
directive, and theupdateDescription()
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.
Posted on October 10, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 27, 2024
November 21, 2024