The Need for Enums in PHP: Pre-8.1 Solutions
How we represented fixed value sets before PHP 8.1 introduced native enums, and the trade-offs of each approach.
PHP 8.1 is bringing native enum support — but it wasn’t out yet when I wrote this. In the meantime, I’d spent years representing fixed sets like “order status”, “payment method”, and “user role” using various workarounds. Each one comes with a cost. Looking at how we filled this gap also shows exactly what we stand to gain once native enums arrive.
Why we need enums
Here’s the problem: an order’s status can be one of pending, processing, shipped, or cancelled. If you represent this as a plain string, those four values end up scattered all over the codebase. A typo like "canceld" is easy to miss. Which values are actually valid in the database becomes unclear.
What we need is: a closed set of specific values, an error when something outside that set is used, and a representation the type system can understand.
PHP didn’t address this natively for a long time, so the developer community came up with different solutions.
Approach 1: Class constants
The most common and simplest route:
<?php
class OrderStatus
{
const PENDING = 'pending';
const PROCESSING = 'processing';
const SHIPPED = 'shipped';
const CANCELLED = 'cancelled';
public static function values(): array
{
return [
self::PENDING,
self::PROCESSING,
self::SHIPPED,
self::CANCELLED,
];
}
public static function isValid(string $status): bool
{
return in_array($status, self::values(), true);
}
}
Usage:
<?php
$order->status = OrderStatus::PENDING;
if (!OrderStatus::isValid($order->status)) {
throw new InvalidArgumentException("Invalid order status: {$order->status}");
}
What’s good: Simple, zero dependencies, IDE autocompletion works fine.
What’s bad: No type safety. If a method expects OrderStatus::PENDING, you’re forced to type-hint string. Any string can be passed; you won’t catch the error at compile or boot time — only at runtime.
Approach 2: The myclabs/php-enum package
This package lets you create enum-like objects:
<?php
use MyCLabs\Enum\Enum;
class OrderStatus extends Enum
{
const PENDING = 'pending';
const PROCESSING = 'processing';
const SHIPPED = 'shipped';
const CANCELLED = 'cancelled';
}
Usage:
<?php
function processOrder(Order $order, OrderStatus $status): void
{
$order->status = $status->getValue();
}
processOrder($order, OrderStatus::PENDING()); // Returns an object
processOrder($order, 'pending'); // Throws TypeError
What’s good: You can now type-hint OrderStatus in your signatures. Pass a wrong value and PHP will throw. On top of that, an equals() method is included for comparisons.
What’s bad: Every enum value is actually an object. You need to be careful with comparisons — use equals(), not ===. And at the end of the day this is a Composer package, not a language feature.
Approach 3: Backed classes (manual value objects)
On some projects I preferred not to depend on myclabs/php-enum and instead wrote my own value objects:
<?php
final class OrderStatus
{
private string $value;
private function __construct(string $value)
{
$this->value = $value;
}
public static function pending(): self
{
return new self('pending');
}
public static function processing(): self
{
return new self('processing');
}
public static function shipped(): self
{
return new self('shipped');
}
public static function cancelled(): self
{
return new self('cancelled');
}
public function getValue(): string
{
return $this->value;
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
public function __toString(): string
{
return $this->value;
}
}
What’s good: Zero dependencies. You can type-hint OrderStatus. The class can carry behavioral logic — methods like isCancellable() live right inside it.
What’s bad: A lot of boilerplate. Writing this structure from scratch for every enum gets tedious. Maintenance becomes heavy on projects that have 10–15 enums.
When I reached for each approach
Class constants are enough for small projects and prototypes. If I’m working with a team, or the business logic is genuinely built around these fixed sets, I’d go with myclabs/php-enum or a manual value object.
I chose the manual value object when the enum needed to carry behavior beyond just representing a value. An OrderStatus object might expose a canBeRefunded() or isTerminal() method — that logic doesn’t come from a package; you write it yourself.
What native enums will bring
PHP 8.1 is introducing enums at the language level. Every approach above is a workaround. Once native enums land, the type system will understand them directly, IDE support will be seamless, comparison operators will work naturally, and no package will be needed.
But as of early 2021, that version isn’t out yet. In the meantime you have to pick one of these options. My preference depends on project size and team size — pick a project-wide standard and stick to it consistently.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.