outerweb/enum-helpers
Laravel package that adds practical helpers to PHP enums, so you can quickly expose labels, values, options, and validation rules for forms, select fields, and APIs—reducing repetitive enum boilerplate across your application.
Install
composer require outerweb/enum-helpers
No service provider or config publish is required — the package is a set of traits for PHP 8.1+ enums, so once Composer has it, you're done.
Where to look first
src/ directory listing — each file is a single trait (HasLabel, HasColor, HasOptions, HasDescription, HasIcon, etc.). Traits are named after the behavior they add, so the file list is the feature list.label() method on your enums, or a values() static helper). Each trait usually has a single abstract method you must implement plus a handful of static helpers it unlocks.tests/ for the exact signature of each helper — this is the fastest, most reliable way to see the API since the README may be thin.First use case — a status enum in a form
namespace App\Enums;
use OuterWeb\EnumHelpers\Traits\HasLabel;
use OuterWeb\EnumHelpers\Traits\HasOptions;
enum OrderStatus: string
{
use HasLabel;
use HasOptions;
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
public function label(): string
{
return match ($this) {
self::Pending => 'Awaiting payment',
self::Paid => 'Paid',
self::Shipped => 'Shipped',
};
}
}
Then in a controller/Blade view you immediately get a value => label map ready for a <select>:
<select name="status">
@foreach (OrderStatus::options() as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
That's the 80% payback: stop re-writing array_column(self::cases(), 'label', 'value') in every controller.
Standardize one "label" contract across all enums. Adopt HasLabel on every enum that has a UI surface. Your views, API resources, and CSV exports can then call $enum->label() without a match block at each call site. Consistency is the real win here.
Build select options in one call. HasOptions (often paired with HasValues) lets you do:
OrderStatus::options(); // ['pending' => 'Awaiting payment', ...]
Use it directly in Blade components, Filament options, or Livewire #[Validate] attributes.
Validation via the enum rule. Backed enums already work with Rule::enum(OrderStatus::class). The helper traits are complementary — use the native Rule::enum() for validation and the traits for presentation. Don't reinvent validation with ->values() unless you're on an older Laravel.
Filter/compare without boilerplate.
$order->status->is(OrderStatus::Paid);
$order->status->isNot(OrderStatus::Pending);
OrderStatus::values(); // for in: rules or query scopes
Group by domain. Put a HasLabel + HasColor combo on status/priority-type enums, and reserve HasOptions/HasValues for enums surfaced as input. Keeping the trait set per enum tight makes the contract obvious.
Pair with route model binding and API resources. Because labels stay on the enum, cast the enum on the model ($casts = ['status' => OrderStatus::class]) and serialize $resource->status->label() in your API resource — one source of truth for a human-readable string.
Reuse in factory states and seeders. Iterate OrderStatus::cases() (or ->values()) instead of hardcoding strings — it makes adding a new case a one-line change and surfaces broken assumptions immediately.
Method name collisions are the #1 pitfall. If you already define label(), color(), or description() on your enum, the trait will either conflict or be silently shadowed depending on definition order. Rename your method or drop the trait — PHP does not warn on trait/method name reuse in a friendly way.
Abstract-method contracts differ per trait. Some traits expect you to implement a method that returns a string, others an array or a config key. Check tests/ or the trait source before assuming label() is the required method — the error you'll get from a missing abstract method is fine, but a mismatched return type in a match is easy to miss in a big enum.
Static vs. instance confusion. ->label() is instance; ::options() and ::values() are static. Calling a static method through an instance (or vice versa) throws an Error, not a warning — and autocompletion will lie to you if your stub types are loose. Lean on PHPStan/Psalm to catch this.
Backed vs. unit enums. options() needs ->value for the key. On a pure (unbacked) unit enum, options() will either throw or fall back to the case name. If you plan to use HasOptions, prefer backed enums — you almost always want the string in form submission.
Localization is your job. These traits rarely call __() for you. Wrap your label() return in __('enums.status.pending') so translations don't require touching every enum:
return __('enums.order_status.' . $this->value);
Do this at the enum level, not the trait level — you keep the enum as the single translation anchor.
Don't reflect at runtime in hot paths. If a trait builds an options array by iterating cases() and calling label() each time, that's fine per-request but wasteful in a loop or a large export. Cache with Cache::remember() if you produce options frequently — or memoize in a local static variable inside the enum.
Small package, small surface. With very low stars/adoption, assume you may outgrow it or need to vendor-patch. Keep the traits shallow (no logic inside label() beyond a match) so replacing the package with your own traits later is a single-file change. Consider it a template as much as a dependency — copy the traits into app/Support/Enums/ if you want zero external dependency and full control.
Testing tip. Write one test that asserts options() count equals count(cases()) for each trait-using enum. It catches the "I added a case but forgot the match arm" bug — which a bare match would otherwise let slip past you until runtime.
IDE support. Add @method PHPDoc stubs at the top of each enum (or rely on your IDE's trait support) so ->label() autocompletes. Without this, the trait is invisible to older IDEs and you'll get red squiggles in views.
How can I help you explore Laravel packages today?