PHP Packagev8.3.0MIT License

#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.

Quick Tip

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.3 (>=8.3 <8.4)
  • No runtime dependencies

#Composer Installation

Terminal
composer require pharaonic/php-smart-enum

Composer autoloads the Pharaonic\SmartEnum namespace. The only class you use is the Pharaonic\SmartEnum\SmartEnum trait.

Installation Complete

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.

app/Enums/Status.php
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::Active
3Status::tryFrom('missing'); // null

from() 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]); // true
10$status->isActive(); // true

See 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::Active
2Status::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'); // true
2Status::hasValue('active'); // true
3Status::hasValue('Active'); // false
4 
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); // true
2Status::Active->eq(Status::Disabled); // false

#is() 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); // true
4$status->is('Active'); // true (case name)
5$status->is('active'); // true (backing value)
6$status->is('ACTIVE'); // false
7$status->isNot(Status::Disabled); // true

The 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 = 1
2 
3$priority->is(1); // true
4$priority->is('1'); // false
A name that is another case's value

Because 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]); // true
2$status->in(['active', 'pending']); // true
3$status->notIn([Status::Disabled]); // true
4$status->in([]); // false

#info()

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(); // true
4$status->isPending(); // false
5$status->isDisabled(); // false

Any 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: PENDING becomes isPending, IN_COOKING becomes isInCooking.
  • Any other word keeps its letters and gets an upper-case first letter: onHold becomes isOnHold, HTTPError becomes isHTTPError, Level2 becomes isLevel2.

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:

app/Enums/Status.php
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.

app/Enums/Status.php
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(): string
10 {
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.

src/helpers.php
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 names
12}

The function must be loaded before any label is read. Load it with Composer's files autoloading:

composer.json
1{
2 "autoload": {
3 "files": ["src/helpers.php"]
4 }
5}
Translations

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:

app/Enums/Priority.php
1enum Priority: int // backed: every case has a value
2{
3 use SmartEnum;
4 
5 case Low = 0;
6 case High = 1;
7}
app/Enums/Direction.php
1enum Direction // unit: cases have a name only
2{
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:

HelperBacked enumUnit enum
values()Backing valuesCase names
options()value => labelname => label
hasValue($value)Checks the backing valuesChecks the case names
info()['value']The backing valueThe case name
is() / in()Case, name or valueCase or name
1Direction::values(); // ['Up', 'Down']
2Direction::options(); // ['Up' => 'Up', 'Down' => 'Down']
3Direction::hasValue('Up'); // true
4Direction::Up->info(); // ['name' => 'Up', 'value' => 'Up', 'label' => 'Up']
Native methods on unit enums

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

MethodDescriptionReturns
names()Case namesarray (list of string)
labels()Case labelsarray (list of string)
values()Backing values; case names on a unit enumarray (list of int|string)
options()Labels keyed by backing value; by case name on a unit enumarray (int|string => string)
fromName(string $name)The case with this case name, or a ValueErrorstatic
tryFromName(string $name)The case with this case name, or null?static
hasName(string $name)Whether a case with this name existsbool
hasValue(string|int $value)Whether a case with this backing value exists; this name on a unit enumbool

#Instance methods

MethodDescriptionReturns
label()Human-readable label of the casestring
eq(UnitEnum $enum)Whether the given case is this casebool
is(UnitEnum|string|int $value)Matches this case, its name or its backing valuebool
isNot(UnitEnum|string|int $value)Inverse of is()bool
in(array $values)Matches any of the given valuesbool
notIn(array $values)Inverse of in()bool
info()['name' => …, 'value' => …, 'label' => …]; value is the case name on a unit enumarray
is<Case>()Whether this is the named case, through __call()bool

#Exceptions

Thrown byExceptionWhen
fromName()ValueErrorNo case has the given name
is<Case>()BadMethodCallExceptionTwo cases produce the same check name
__call()ErrorThe name is not a check, or is a non-public method called from outside the enum

#Label hook

FunctionDescriptionReturns
smart_enum_label(UnitEnum $case)Optional global function you define; used by label() when it returns a string?string

#Native methods (provided by PHP)

MethodDescriptionAvailable on
cases()All cases, in declaration orderUnit and backed enums
from(int|string $value)The case with this backing value, or a ValueErrorBacked enums
tryFrom(int|string $value)The case with this backing value, or nullBacked enums

#Examples

#1. A select input

Build <option> tags from options(), which maps each backing value to its label.

app/Enums/Status.php
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}
templates/status-select.php
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().

src/Http/UpdateStatus.php
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.

src/Accounts/Account.php
1public function suspend(): void
2{
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.

config/app.php
1$defaultStatus = Status::tryFromName(getenv('DEFAULT_STATUS') ?: '') ?? Status::Pending;

#5. Serializing for an API

Return the case metadata from info() in a JSON response.

src/Http/StatusController.php
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:

app/Enums/Status.php
1enum Status: string
2{
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.3 (>=8.3 <8.4). 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!

Moamen Eltouny

@MoamenEltouny

13 contributions

Want to Contribute?

We welcome contributions from everyone! Whether it's bug fixes, new features, or documentation improvements - every contribution counts.