eloquent/enumeration
Eloquent Enumeration brings typed enum support to Laravel’s Eloquent models. Define reusable enums, cast attributes to enum instances, and keep status or type fields type-safe and expressive without leaving familiar Eloquent conventions.
eloquent/enumeration is a plain-PHP enumeration library — no service provider, no facade, no Laravel coupling. Install it and start typing constants.
composer require eloquent/enumeration
First places to look:
README.md for the AbstractEnumeration / Multiton split.src/AbstractEnumeration.php — the class you will extend in ~90% of cases.src/Multiton.php / src/AbstractMultiton.php — only if you need non-scalar member values.The canonical first use case is a status column. In app/Enums/OrderStatus.php:
namespace App\Enums;
use Eloquent\Enumeration\AbstractEnumeration;
final class OrderStatus extends AbstractEnumeration
{
const PENDING = 'pending';
const PAID = 'paid';
const SHIPPED = 'shipped';
const CANCELLED = 'cancelled';
}
You then get both the raw constant (OrderStatus::PAID === 'paid') and a singleton instance via the magic static call (OrderStatus::PAID()). The instance exposes value(), key(), __toString(), is(), isNot(), compareTo(), plus class-level helpers such as members(), values(), keys(), valueOf(), valueOfKey(), isValidValue(), and isValidKey(). Confirm the exact helper list against the README of the version you install — it shifted between 5.x and 6.x.
A good first refactor: find every const STATUS_PAID = 'paid' or bare 'paid' string in models/services and move it into one enum class per domain concept.
Custom Eloquent cast (Laravel 7+). The package predates CastsAttributes, so write a thin adapter once and reuse it:
class EnumerationCast implements CastsAttributes
{
public function __construct(protected string $enumClass) {}
public function get($model, $key, $value, $attributes)
{
return $value === null ? null : $this->enumClass::valueOf($value);
}
public function set($model, $key, $value, $attributes)
{
if ($value === null) return null;
return $value instanceof $this->enumClass ? $value->value() : (string) $value;
}
}
// In the model:
protected $casts = ['status' => EnumerationCast::class.':'.OrderStatus::class];
This is the single highest-leverage integration: attributes come back as enum objects, assignments accept either the object or the scalar, and the database still sees a string.
Validation. Wrap isValidValue() in a rule object so requests never carry string arrays:
class EnumerationRule implements ValidationRule
{
public function __construct(private string $enumClass) {}
public function validate($attribute, $value, $fail): void
{
if (!$this->enumClass::isValidValue($value)) {
$fail("The {$attribute} is not a valid {$this->enumClass}.");
}
}
}
Use Rule::in(OrderStatus::values()) for the simple in: case, and the rule object when you need a custom message.
Dropdowns and API resources. OrderStatus::members() gives you instances; array_map(fn ($m) => ['value' => $m->value(), 'label' => $m->key()], OrderStatus::members()) is your select-options payload. In API resources, always output ->value(), never the object.
Query building. Order::where('status', OrderStatus::PAID()->value()) inside a scope or repository. Prefer this over comparing to constants so refactors are single-source.
State transitions. is() / isNot() read much better than === chains in service classes:
if ($order->status->isNot(OrderStatus::PENDING())) {
throw new DomainException('Only pending orders can be paid.');
}
compareTo() is useful when you deliberately order members in the class body (pending < paid < shipped) and want sortable semantics.
Multiton for rich values. If members carry more than a scalar (e.g., a currency with symbol and precision), extend AbstractMultiton and register members in the static initialize() hook. This is the main reason to choose this package over myclabs/php-enum: object-valued members with identity semantics.
Testing. Factories should accept the enum and let the cast flatten it: 'status' => OrderStatus::PAID(). In assertions, compare ->value() rather than the object, since the singleton guarantee only holds within one process.
OrderStatus::PAID is the string 'paid'; OrderStatus::PAID() is the instance. Half your bugs will be a missing (). Enforce a convention across the team and grep for ::[A-Z_]+[^\(] in reviews.strlen(null), implicit nullable parameters) before adopting on PHP 8.2+, and pin the version in composer.json.enum OrderStatus: string covers the scalar case with better static analysis and IDE support. Keep the enum classes thin and the cast adapter central so the swap is mechanical. Keep this package only where you need Multiton's object values or you're on PHP < 8.1.->value() first — the deserialized object may not be === to the canonical member, breaking is()-based comparisons downstream.$casts doesn't support the class directly. Assigning an enum object to a raw attribute lets PDO receive an object; anything without __toString in the binding path will error. Always go through the custom cast.valueOf() throws. It raises UnexpectedValueException for unknown values. In request-handling paths use isValidValue() first or catch and convert to a 422; don't let it bubble into a 500.'0' or '' will trip naive if ($enum) checks. isValidValue() uses strict comparison, but your own code may not — compare with !== null.AbstractEnumeration for scalar members, AbstractMultiton for object members, and the trait variant only when your class already extends something else. Mixing paradigms in one codebase makes the casts and rules non-uniform.OrderStatus::options() or OrderStatus::labels(), add them as static helpers on a shared base enum class in your app — the package has no extension-point config.CLASS, FUNCTION, TRAIT and friends are legal as class constants in modern PHP but have caused keyword collisions in older parsers. If a member name reads like a keyword, rename the constant rather than fight the toolchain.How can I help you explore Laravel packages today?