Smart Enum
Smart, lightweight helpers for native PHP enums. Add the SmartEnum trait to any enum you already have and get helpers for case names, backing values, labels, select options, lookup by case name, comparisons and is<Case>() checks. Native methods (cases(), from(), tryFrom()) stay native, and the package has no base class, no generated code and no runtime dependencies.
Names, Values & Labels
List the case names, the backing values, or the labels of every case.
Select Options
Map each backing value to a human-readable label, ready for a dropdown.
Lookup by Case Name
Find a case by its name, the same way from() finds one by its value.
Comparisons
Compare a case with another case, a case name or a backing value, always strictly.
Case Checks
Ask a case about itself with isActive(), isPending() and one check per case.
Unit & Backed Enums
The same trait works on unit, string-backed and int-backed enums.
Keep using PHP's own Status::cases(), Status::from() and Status::tryFrom(). SmartEnum only adds what PHP doesn't have.
Installation
Install the package with Composer. There is nothing to register or configure.
Requirements
- PHP 8.4 (
>=8.4 <8.5) - No runtime dependencies
Composer Installation
composer require pharaonic/php-smart-enumComposer autoloads the Pharaonic\SmartEnum namespace. The only class you use is the Pharaonic\SmartEnum\SmartEnum trait.
You're all set! Add use SmartEnum; to any of your enums.
Basic Usage
Add the trait
Import the trait and use it inside a native enum. Backed and unit enums work the same way.
1<?php 2 3namespace App\Enums; 4 5use Pharaonic\SmartEnum\SmartEnum; 6 7enum Status: string 8{ 9 use SmartEnum;10 11 case Pending = 'pending';12 case Active = 'active';13 case Disabled = 'disabled';14}Native methods stay native
PHP itself provides these methods. SmartEnum does not redefine or wrap them:
1Status::cases(); // [Status::Pending, Status::Active, Status::Disabled]2Status::from('active'); // Status::Active3Status::tryFrom('missing'); // nullfrom() and tryFrom() exist on backed enums only.
SmartEnum helpers
The trait adds static helpers on the enum and instance helpers on each case:
1Status::names(); // ['Pending', 'Active', 'Disabled'] 2Status::options(); // ['pending' => 'Pending', 'active' => 'Active', 'disabled' => 'Disabled'] 3Status::fromName('Active'); // Status::Active 4 5$status = Status::Active; 6 7$status->label(); // 'Active' 8$status->is('active'); // true 9$status->in([Status::Active, Status::Pending]); // true10$status->isActive(); // trueSee Static Methods, Instance Methods and Case Checks for each one.
Static Methods
Static helpers are called on the enum itself. The examples use the Status enum from Basic Usage.
names()
Returns the case names, in declaration order.
1Status::names(); // ['Pending', 'Active', 'Disabled']values()
Returns the backing values, in declaration order.
1Status::values(); // ['pending', 'active', 'disabled']On a unit enum it returns the case names instead. See Unit vs Backed Enums.
labels()
Returns the case labels, in declaration order.
1Status::labels(); // ['Pending', 'Active', 'Disabled']Use options() when you need each label keyed by its value.
Each label comes from the case's label() method, so custom labels show up here too.
options()
Returns each case's label, keyed by backing value (value => label), for select inputs.
1Status::options(); // ['pending' => 'Pending', 'active' => 'Active', 'disabled' => 'Disabled']On a unit enum the keys are the case names.
fromName() and tryFromName()
Find a case by its case name, the way from() and tryFrom() find one by its backing value. The name must match exactly, including letter case. fromName() throws a ValueError when no case matches; tryFromName() returns null.
1Status::fromName('Active'); // Status::Active2Status::tryFromName('active'); // null (that's a backing value, not a name)3Status::fromName('Archived'); // ValueError: No case with name Archived in enum App\Enums\Status.hasName() and hasValue()
Check whether a case name, or a backing value, exists. Both comparisons are strict.
1Status::hasName('Active'); // true2Status::hasValue('active'); // true3Status::hasValue('Active'); // false4 5Priority::hasValue(1); // true (int-backed)6Priority::hasValue('1'); // false (no type juggling)On a unit enum, hasValue() checks the case names.
Instance Methods
Instance helpers are called on a case. The examples use the Status enum from Basic Usage.
label()
Returns the human-readable label of the case. By default it is the case name.
1Status::Active->label(); // 'Active'See Labels to change it.
eq()
Strict comparison with another enum case. It only accepts an enum case, and a case of another enum never matches, even with the same name.
1Status::Active->eq(Status::Active); // true2Status::Active->eq(Status::Disabled); // falseis() and isNot()
A comparison that accepts an enum case, a case name, or a backing value (string or int). isNot() is the inverse.
1$status = Status::Active;2 3$status->is(Status::Active); // true4$status->is('Active'); // true (case name)5$status->is('active'); // true (backing value)6$status->is('ACTIVE'); // false7$status->isNot(Status::Disabled); // trueThe matching rules:
- An enum case matches only when it is the same case, like
eq(). - A string or an int matches when it equals this case's own name or backing value.
- Comparison is strict: an int-backed case matches
1, never'1'.
1$priority = Priority::High; // case High = 12 3$priority->is(1); // true4$priority->is('1'); // falseBecause is() looks at the case's own name and value only, a string can match two cases when one case's name is another case's backing value. With case Open = 'Closed' and case Closed = 'Open', both Status::Open->is('Open') and Status::Closed->is('Open') are true. Pass the case itself, or use eq(), when you need an exact match.
in() and notIn()
Check whether the case matches any of the given values. Each value is matched like the argument of is(), and the list may mix cases, names and values. notIn() is the inverse. An empty list never matches.
1$status->in([Status::Active, Status::Pending]); // true2$status->in(['active', 'pending']); // true3$status->notIn([Status::Disabled]); // true4$status->in([]); // falseinfo()
Returns metadata about the case: its name, backing value and label.
1Status::Active->info(); // ['name' => 'Active', 'value' => 'active', 'label' => 'Active']On a unit enum, value is the case name.
Case Checks
Every case gets an is<Case>() check, resolved by the trait's __call(). It returns true only on the case it is named after.
1$status = Status::Active;2 3$status->isActive(); // true4$status->isPending(); // false5$status->isDisabled(); // falseAny arguments you pass to a check are ignored.
How check names are built
The check name is is followed by the case name, with each underscore-separated word capitalized:
- A word written entirely in upper case is title-cased:
PENDINGbecomesisPending,IN_COOKINGbecomesisInCooking. - Any other word keeps its letters and gets an upper-case first letter:
onHoldbecomesisOnHold,HTTPErrorbecomesisHTTPError,Level2becomesisLevel2.
Check names are matched exactly. isPENDING(), isonHold(), isHttpError() and IsPending() are not checks, and calling them throws Error: Call to undefined method.
Ambiguous checks
When two cases produce the same check name, for example IN_PROGRESS and InProgress (both isInProgress), calling that check throws a BadMethodCallException that names both cases. The checks of the other cases keep working.
1Task::Done->isInProgress();2// BadMethodCallException: Call to ambiguous method App\Enums\Task::isInProgress(): matches cases IN_PROGRESS, InProgress.Use is() with the case itself when the names collide: $task->is(Task::InProgress).
Your own methods win
A method you declare in the enum is always called directly, never through __call(). If you declare isActive() yourself, your version is used.
A call that matches no check throws Error: Call to undefined method, the same error PHP throws without the trait. That includes your enum's own protected and private methods: calling one from outside the enum fails, as visibility says it should.
IDE and static analysis support
IDEs and PHPStan can't see __call() checks on their own. Declare them with @method tags on the enum:
1/** 2 * @method bool isPending() 3 * @method bool isActive() 4 * @method bool isDisabled() 5 */ 6enum Status: string 7{ 8 use SmartEnum; 9 10 case Pending = 'pending';11 case Active = 'active';12 case Disabled = 'disabled';13}Labels
label() returns the case name by default. labels(), options() and info() all read their labels from label(), so changing it once changes them all.
Per enum: override label()
Declare label() in the enum. A method declared in the enum takes priority over the trait's.
1enum Status: string 2{ 3 use SmartEnum; 4 5 case Pending = 'pending'; 6 case Active = 'active'; 7 case Disabled = 'disabled'; 8 9 public function label(): string10 {11 return match ($this) {12 self::Pending => 'Waiting for review',13 self::Active => 'Active',14 self::Disabled => 'Disabled',15 };16 }17}18 19Status::options(); // ['pending' => 'Waiting for review', 'active' => 'Active', 'disabled' => 'Disabled']App-wide: the smart_enum_label() hook
When a global function named smart_enum_label() exists, the trait's label() calls it with the case. If it returns a string, that string is the label. If it returns anything else, such as null, the case name is used.
1<?php 2 3use App\Enums\Status; 4 5function smart_enum_label(UnitEnum $case): ?string 6{ 7 if ($case instanceof Status) { 8 return ucwords(strtolower(str_replace('_', ' ', $case->name))); 9 }10 11 return null; // other enums keep their case names12}The function must be loaded before any label is read. Load it with Composer's files autoloading:
1{2 "autoload": {3 "files": ["src/helpers.php"]4 }5}The hook is a good place to translate labels, for example by looking the case up in your translation files. An enum that overrides label() itself does not call the hook.
Unit vs Backed Enums
PHP has two kinds of enums, and SmartEnum supports both:
1enum Priority: int // backed: every case has a value2{3 use SmartEnum;4 5 case Low = 0;6 case High = 1;7}1enum Direction // unit: cases have a name only2{3 use SmartEnum;4 5 case Up;6 case Down;7}Helpers that only need the case name work the same on both: names(), labels(), fromName(), tryFromName(), hasName(), label(), eq() and the is<Case>() checks.
A unit enum has no backing values, so the helpers that use them fall back to the case name:
| Helper | Backed enum | Unit enum |
|---|---|---|
values() | Backing values | Case names |
options() | value => label | name => label |
hasValue($value) | Checks the backing values | Checks the case names |
info()['value'] | The backing value | The case name |
is() / in() | Case, name or value | Case or name |
1Direction::values(); // ['Up', 'Down']2Direction::options(); // ['Up' => 'Up', 'Down' => 'Down']3Direction::hasValue('Up'); // true4Direction::Up->info(); // ['name' => 'Up', 'value' => 'Up', 'label' => 'Up']PHP does not define from() or tryFrom() on unit enums. Use fromName() / tryFromName() to look up a unit case by name.
API Reference
All methods are declared on the Pharaonic\SmartEnum\SmartEnum trait.
Static methods
| Method | Description | Returns |
|---|---|---|
names() | Case names | array (list of string) |
labels() | Case labels | array (list of string) |
values() | Backing values; case names on a unit enum | array (list of int|string) |
options() | Labels keyed by backing value; by case name on a unit enum | array (int|string => string) |
fromName(string $name) | The case with this case name, or a ValueError | static |
tryFromName(string $name) | The case with this case name, or null | ?static |
hasName(string $name) | Whether a case with this name exists | bool |
hasValue(string|int $value) | Whether a case with this backing value exists; this name on a unit enum | bool |
Instance methods
| Method | Description | Returns |
|---|---|---|
label() | Human-readable label of the case | string |
eq(UnitEnum $enum) | Whether the given case is this case | bool |
is(UnitEnum|string|int $value) | Matches this case, its name or its backing value | bool |
isNot(UnitEnum|string|int $value) | Inverse of is() | bool |
in(array $values) | Matches any of the given values | bool |
notIn(array $values) | Inverse of in() | bool |
info() | ['name' => …, 'value' => …, 'label' => …]; value is the case name on a unit enum | array |
is<Case>() | Whether this is the named case, through __call() | bool |
Exceptions
| Thrown by | Exception | When |
|---|---|---|
fromName() | ValueError | No case has the given name |
is<Case>() | BadMethodCallException | Two cases produce the same check name |
__call() | Error | The name is not a check, or is a non-public method called from outside the enum |
Label hook
| Function | Description | Returns |
|---|---|---|
smart_enum_label(UnitEnum $case) | Optional global function you define; used by label() when it returns a string | ?string |
Native methods (provided by PHP)
| Method | Description | Available on |
|---|---|---|
cases() | All cases, in declaration order | Unit and backed enums |
from(int|string $value) | The case with this backing value, or a ValueError | Backed enums |
tryFrom(int|string $value) | The case with this backing value, or null | Backed enums |
Examples
1. A select input
Build <option> tags from options(), which maps each backing value to its label.
1<?php 2 3namespace App\Enums; 4 5use Pharaonic\SmartEnum\SmartEnum; 6 7enum Status: string 8{ 9 use SmartEnum;10 11 case Pending = 'pending';12 case Active = 'active';13 case Disabled = 'disabled';14}1<select name="status">2 <?php foreach (\App\Enums\Status::options() as $value => $label): ?>3 <option value="<?= htmlspecialchars((string) $value) ?>"><?= htmlspecialchars($label) ?></option>4 <?php endforeach ?>5</select>2. Validating input
Check a submitted value before converting it with PHP's own from().
1if (! Status::hasValue($input['status'])) {2 throw new InvalidArgumentException('Unknown status.');3}4 5$status = Status::from($input['status']);3. Guarding a state change
Allow an action only for some cases.
1public function suspend(): void2{3 if ($this->status->notIn([Status::Pending, Status::Active])) {4 throw new LogicException('Only pending or active accounts can be suspended.');5 }6 7 $this->status = Status::Disabled;8}4. Configuration by case name
Read a case name from configuration or the environment and resolve it, with a fallback.
1$defaultStatus = Status::tryFromName(getenv('DEFAULT_STATUS') ?: '') ?? Status::Pending;5. Serializing for an API
Return the case metadata from info() in a JSON response.
1echo json_encode(array_map(2 static fn (Status $status): array => $status->info(),3 Status::cases(),4));Troubleshooting
Error: Call to undefined method Status::isactive()
Check names are matched exactly, including letter case: write isActive(), not isactive() or IsActive(). For a case named IN_COOKING, the check is isInCooking(). See How check names are built.
BadMethodCallException: Call to ambiguous method
Two of your cases produce the same check name, such as IN_PROGRESS and InProgress. Rename one of them, or compare with $task->is(Task::InProgress).
PHPStan or my IDE reports isActive() as undefined
is<Case>() checks are resolved at runtime by __call(). Add @method bool isActive() tags to the enum's docblock, as shown in IDE and static analysis support.
is('1') returns false on an int-backed enum
Matching is strict. Pass the integer: $priority->is(1). Convert request input first, for example with Priority::tryFrom((int) $input).
My smart_enum_label() function is ignored
The function must be global (no namespace), loaded before the label is read (use Composer files autoloading), and return a string. An enum that declares its own label() does not call it.
My own label() method is used instead of the trait's
PHP gives a method declared in the enum priority over a method from a trait. This is expected: if your enum declares label(), is() or another method with the same name, your version wins. Keep the same signature so code that relies on the trait keeps working.
Trait method collision with another trait
If another trait used by the same enum also declares, for example, is(), PHP throws a fatal error. Resolve it with insteadof:
1enum Status: string2{3 use SmartEnum, OtherTrait {4 SmartEnum::is insteadof OtherTrait;5 }6 7 case Active = 'active';8}Call to undefined method Direction::from()
from() and tryFrom() are native PHP methods that only exist on backed enums. For a unit enum, look cases up by name with fromName() / tryFromName().
Composer refuses to install the package
This release line requires PHP 8.4 (>=8.4 <8.5). Each PHP minor from 8.1 has its own release line, and Composer installs the one that matches your PHP; 8.1 is the version that introduced native enums. Check php -v and the config.platform.php setting in your composer.json.
Contributors
Amazing people who made this package possible
This package wouldn't exist without the contributions of our amazing community. A huge thank you to everyone who has helped improve Smart Enum!
Want to Contribute?
We welcome contributions from everyone! Whether it's bug fixes, new features, or documentation improvements - every contribution counts.