bensondevs/supercharged-enums

Supercharged backed enum helpers (find, options, comparisons) with no framework dependencies.

Maintainers

Package info

github.com/bensondevs/supercharged-enums

pkg:composer/bensondevs/supercharged-enums

Transparency log

Statistics

Installs: 429

Dependents: 1

Suggesters: 0

Stars: 5

Open Issues: 0

0.2.0 2026-06-23 07:57 UTC

This package is auto-updated.

Last update: 2026-07-13 12:46:44 UTC


README

CI Stable Version Total Downloads Type Coverage

Backed enum helpers (find, options, comparisons, labels) via the EnumExtension trait—no framework dependencies. The package also ships optional ready-made enums for everyday domains (HTTP, calendar and time, measurement units, finance, logging, deployment environments, and more) under BensonDevs\SuperchargedEnums\Common\, each wired with the same helpers. See Bundled Common enums.

Requirements: PHP 8.2 or later. The EnumExtension trait targets backed enums (string or int). Pure unit enums without a backing type are not supported by lookup normalization.

Table of contents

Installation

composer require bensondevs/supercharged-enums

Laravel Boost

This package ships Laravel Boost AI resources for coding agents:

  • Skill (supercharged-enums-development) — on-demand patterns for EnumExtension, lookup, select maps, comparisons, and bundled Common\ enums
  • Guidelines — always-loaded overview and conventions

Boost discovers these automatically from resources/boost/ in the installed package. No ServiceProvider or extra configuration is required in this library.

In a Laravel project that uses Boost:

composer require laravel/boost --dev   # if not already installed
php artisan boost:install              # select skills; enable bensondevs/supercharged-enums
# or, for an existing Boost setup:
php artisan boost:update --discover

Boost integration is opt-in on the consumer side. The package remains framework-agnostic.

Laravel Artisan stubs

For Laravel projects, you can publish a customized enum.backed.stub so php artisan make:enum generates backed enums with EnumExtension already applied.

Enable (one-time, manual):

php artisan vendor:publish --tag=supercharged-enums-stubs

Generate enums as usual:

php artisan make:enum Status --string
# or --int

Example output:

use BensonDevs\SuperchargedEnums\EnumExtension;

enum Status: string
{
    use EnumExtension;

    //
}

Notes:

  • Laravel-only and opt-in — publishing is required; non-Laravel projects are unaffected.
  • Only backed enums (--string / --int) use this stub. Pure unit enums are unchanged.
  • If your app already has stubs/enum.backed.stub (for example from php artisan stub:publish), use --force to overwrite — that replaces your existing customization.
  • Stub publishing is separate from Laravel Boost; Boost skills and guidelines do not require publishing stubs.

Beta: Enum import commands are experimental. APIs and generated output may change in minor releases.

Import enum from a legacy lookup table (beta)

When migrating from database lookup tables to backed enums, generate a ready-to-use enum from existing rows:

php artisan supercharged-enums:import-from-table order_statuses \
  --class=OrderStatus          # optional; defaults from the table name
  --string|--int               # optional; overrides auto backing detection
  --value-column=name          # optional; overrides auto-detection
  --label-column=title         # optional
  --aliases                    # optional; emit alias() for legacy keys
  --path=app/Enums             # optional
  --force                      # overwrite an existing file

Column auto-detection defaults:

Purpose Detection order
Backing value slug, code, name, then id
Human label label, title, description (skips the value column)
Case order sort_order, position, order, then id

Backing type: auto-detects int when the value column is id (or an integer column); otherwise string. Use --string or --int to override.

Generated output includes EnumExtension, one case per table row, optional getLabel() when a label column exists, and optional alias() with --aliases (maps legacy integer IDs for string-backed enums, or legacy string keys for int-backed enums).

A progress bar is shown while rows are read, followed by a summary of unique cases found.

This command is Laravel-only and requires a database connection. It does not drop the legacy table or update models automatically. This feature is currently beta.

Enum importers (beta)

For repeatable imports across multiple tables, query filters, or custom column mappings, scaffold an importer class:

php artisan make:enum-importer OccupancyEnumImporter

This creates app/EnumImporters/OccupancyEnumImporter.php extending EnumImporter. Configure sources and optional resolvers:

public function sources(): array
{
    return [
        'occupancy_types',
        'legacy_occupancies' => fn (Builder $query) => $query->where('active', true),
    ];
}

public function resolveUsing(): array
{
    return [
        'occupancy_types' => fn (array $attributes) => [
            'id' => $attributes['id'],
            'value' => $attributes['code'],
            'name' => $attributes['code'],
            'label' => $attributes['name'],
            'sort' => $attributes['id'],
        ],
    ];
}

// public function as(): string { return 'PowerfulEnum'; }
// public function onDuplicate(): string { return 'last-wins'; }

Run the importer:

php artisan supercharged-enums:import-enum-using OccupancyEnumImporter

Importer API:

Method Purpose
sources() Table names, optionally with query closures
resolveUsing() Per-table row resolver returning value, name, label, id, sort from $attributes['column']
as() Target enum class name (default: OccupancyEnumImporterOccupancyEnum)
onDuplicate() fail (default) or last-wins when backing values collide across sources
aliases() Emit alias() mappings for legacy keys

Overwrite behavior: if the enum file already exists, the command prompts for confirmation. Use --force to skip the prompt, or --no-interaction to fail without overwriting (CI-safe). Override duplicate handling per run with --duplicates=last-wins.

Both import commands show a progress bar while reading rows and report how many unique cases were found.

Use import-from-table for one-off imports; use importers when the same migration will be re-run or spans multiple sources. This feature is currently beta.

Quick start

Add the trait to your own enum, or use a bundled one from Common\—same helpers either way.

Your enum

use BensonDevs\SuperchargedEnums\EnumExtension;

enum Status: string
{
    use EnumExtension;

    case Draft = 'draft';
    case Published = 'published';
}

Lookup — request / DB strings → cases (null when unknown)

Status::find('published');               // Status::Published
Status::findOrDefault('archived');         // Status::Draft (falls back to default)
Status::find('published', strict: true);   // Backing values only, no aliases

Labels and forms

Status::options();                // ['draft' => 'Draft', 'published' => 'Published']
Status::Published->getName();     // "Published"

Comparisons — declaration order, not backing-value sort

Status::Draft->is('draft');                      // true
Status::Draft->isBefore(Status::Published);      // true
Status::Draft->next();                           // Status::Published

Bundled time unitsDurationUnit with conversions and the same helpers

use BensonDevs\SuperchargedEnums\Common\Time\DurationUnit;

DurationUnit::find('hour');             // DurationUnit::Hour
DurationUnit::findOrDefault('eon');     // DurationUnit::Second (unknown → default)

DurationUnit::Hour->toSeconds(2);       // 7200
DurationUnit::Hour->isBefore(DurationUnit::Week);                        // true
DurationUnit::Hour->isBetween(DurationUnit::Minute, DurationUnit::Day);  // true
DurationUnit::Hour->next();             // DurationUnit::Day

Bundled measurement unitsLengthUnit, MassUnit, and others under Common\Measure

use BensonDevs\SuperchargedEnums\Common\Measure\LengthUnit;

LengthUnit::find('mile');               // LengthUnit::Mile
LengthUnit::Mile->toKilometers(1);      // Convert 1 mile → kilometers
LengthUnit::Mile->toMeters(1, decimalDigits: 4);

LengthUnit::Meter->isBefore(LengthUnit::Kilometer);  // true (declaration order)
LengthUnit::Meter->next();              // LengthUnit::Kilometer

More domains (HTTP status codes, money rounding, data sizes) are listed under Bundled Common enums.

Features

EnumExtension reference. For a walkthrough, see Quick start. To use individual traits, see Modular composition.

Core helpers

Status::default();     // First declared case (cases()[0]); override on your enum to return any case
Status::getDefault();  // Alias for default()
Status::random();      // A random declared case

default() follows declaration order, not the smallest or largest backing value. For example, HttpStatusCode defaults to Continue (100) because it is declared first, even though other codes have smaller numeric semantics in different contexts.

There is no separate config flag: declare the case you want as the default first, or override default() on your enum to return any case you prefer. findOrDefault() and any code that falls back to default() will use whichever you define.

enum Priority: string
{
    use EnumExtension;

    // Declared first → default() returns Priority::Medium
    case Medium = 'medium';
    case Low = 'low';
    case High = 'high';
}

Priority::default();              // Priority::Medium
Priority::findOrDefault('urgent'); // Priority::Medium (unknown key)

// To change the default, reorder cases — put Low first instead:
enum Priority: string
{
    use EnumExtension;

    case Low = 'low';
    case Medium = 'medium';
    case High = 'high';
}

Priority::default(); // Priority::Low

// Or override default() — keep declaration order, pick any case:
enum Priority: string
{
    use EnumExtension;

    case Medium = 'medium';
    case Low = 'low';
    case High = 'high';

    public static function default(): static
    {
        return self::Low;
    }
}

Priority::default(); // Priority::Low

If you use a partial trait stack without EnumExtension, define default() yourself (for example return self::cases()[0];) so findOrDefault() keeps working.

Lookup

Status::find('published');                    // Status::Published — resolve self, backing scalar, or null
Status::find('unknown');                      // null when nothing matches
Status::findOrDefault('unknown');             // Status::Draft — find() or default() when lookup fails
Status::find('published', strict: true);      // Backing values only (no aliases)

find() accepts:

  • An enum instance (returned as-is)
  • null (returns null)
  • A backing value: string keys for string-backed enums; int keys for int-backed enums, with numeric strings coerced to int (e.g. '2'2)

When $strict is false and tryFrom() does not match, cases may define alternate keys via an instance method:

public function alias(): array
{
    return match ($this) {
        self::Active => ['legacy_active'],
        default => [],
    };
}
  • Aliases are consulted only when $strict is false.
  • With $strict true, resolution is limited to tryFrom() (backing values only).
  • If the same alias appears on multiple cases, behavior is undefined; the first match in cases() iteration order wins.
EnumWithAliases::find('legacy_active');              // Active
EnumWithAliases::find('legacy_active', strict: true); // null
EnumWithAliases::findOrDefault('nope');              // First (default case)

Case listing

Status::names();   // ['Draft', 'Published'] — PHP case names
Status::values();  // ['draft', 'published'] — backing values

Naming

$status = Status::Draft;

$status->getKey();  // 'draft' — the backing value ($this->value)
$status->getName(); // 'Draft' — human label derived from the case name

getName() normalizes case names for display: underscores and hyphens become spaces, PascalCase is split, and the result is title-cased (NoShow"No show", FirstOption"First option").

Select maps

Build value => label maps for HTML <select> elements, JSON APIs, and similar UIs.

Status::options();              // ['draft' => 'Draft', 'published' => 'Published'] — backing value → short label
Status::asSelectOptions();      // Alias for options()
Status::asSelectDescriptions(); // Backing value → longer description text
Status::all();                  // [Status::Draft, Status::Published] — filtered cases as array
Status::collect();              // Illuminate\Support\Collection of filtered cases
Status::filteredCases();        // Same cases as all() (alias)

collect() requires illuminate/support (included in Laravel apps). Without it, calling collect() throws a runtime exception with install instructions.

Label resolution (options()), first match wins:

  1. label() instance method
  2. getLabel()
  3. getName()
  4. Raw PHP case name

Description resolution (asSelectDescriptions()):

  1. getDescription()
  2. getLabel()
  3. Raw PHP case name

Filtering which cases appear in maps

Define either an allow-list or a deny-list as a static method returning enum cases and/or backing scalars:

public static function selectables(): array
{
    return [self::Beta, 'gamma'];
}

public static function unselectables(): array
{
    return [self::Hidden, 'archived'];
}
  • When both selectables() and unselectables() exist, selectables() wins.
  • Filtered cases keep declaration order from the enum.

Comparisons and ordering

Operands accept enum instances, backing scalars, or null. Scalars and aliases resolve through find() (non-strict), same as lookup.

Declaration order, not backing values

Ordering methods (compareTo, isBefore, isAfter, next, previous, min, max, and related helpers) use the index in cases(), not numeric or lexical order of backing values. A case declared first with backing value 2 is still “before” a case declared second with backing value 1.

Notes:

  • isIn / isNotIn ignore entries that do not resolve to a case.
  • compareTo returns -1, 0, 1, or null when the other operand does not resolve.
  • isBetween is inclusive on both ends by default; pass includeStart: false or includeEnd: false for exclusive bounds.
  • next / previous return null at the end of the list unless $wrap is true.
  • min / max skip unresolvable operands; return null if none resolve.
use BensonDevs\SuperchargedEnums\Common\Calendar\Month;

$month = Month::March;

Equality

// Backing string (or other scalar the enum resolves)
$month->is('march');                           // true
$month->isNot('december');                     // true

// Enum instance
$month->is(Month::March);                      // true
$month->isNot(Month::December);                // true
$month->isIn(['october', 'march', 'june']);    // true — backing values in the list
$month->isIn([Month::October, Month::March]);   // true — cases in the list
$month->isNotIn(['december']);                 // true
$month->isNotIn([Month::December]);            // true

Order — declaration order, not backing-value sort

// Backing string
$month->isBefore('october');                   // true
$month->isAfter('october');                    // false

// Enum instance
$month->isBefore(Month::October);              // true
$month->isAfter(Month::October);               // false
$month->isBeforeOrEqual(Month::October);       // true
$month->isAfterOrEqual(Month::October);        // false
$month->compareTo('october');                  // -1, 0, 1, or null when other does not resolve
$month->compareTo(Month::October);             // -1

Range — reversed bounds are swapped automatically

$month->isBetween('january', 'december');                    // true (inclusive both ends)
$month->isBetween(Month::January, Month::December);          // true
$month->isBetween(Month::December, Month::January, includeStart: false); // exclusive start

Position

$month->isFirst();                             // false
$month->isLast();                              // false
$month->diff('june');                          // 3
$month->diff(Month::June);                     // 3

Navigationnull at the end unless $wrap is true

$month->next();                                // Month::April
Month::January->previous(wrap: true);          // Month::December (wrap from first)

Aggregates — skip unresolvable operands; null if none resolve

Month::min(Month::March, Month::October);      // Month::March
Month::min('march', 'october');                // Month::March
Month::max(Month::March, 'october');           // Month::October

Runtime supercharge

When you cannot add EnumExtension to an enum (vendor code, generated enums, or one-off usage), wrap it at runtime with supercharge(). PHP cannot attach methods to enum instances dynamically, so this returns a wrapper object — not the native enum type. Use unwrap() when an API expects the raw case.

use function BensonDevs\SuperchargedEnums\supercharge;

enum VendorStatus: string
{
    case Open = 'open';
    case Closed = 'closed';
}

// Instance API — comparisons, naming, navigation
$status = supercharge(VendorStatus::Open);
$status->is('open');              // true
$status->getName();               // "Open"
$status->value;                   // 'open' (via property access)
$status->unwrap();                // VendorStatus::Open

// Static API — lookup, select maps, aggregates (pass the class string)
supercharge(VendorStatus::class)->find('open');           // VendorStatus::Open
supercharge(VendorStatus::class)->findOrDefault('nope'); // VendorStatus::Open (first case)
supercharge(VendorStatus::class)->options();              // ['open' => 'Open', 'closed' => 'Closed']
Scenario Recommended approach
You own the enum use EnumExtension — native Status::Draft->is(...)
Third-party / vendor enum supercharge(VendorEnum::class)->find($key)
One-off comparison supercharge($case)->is('foo')

Notes:

  • Backed enums only — same constraint as EnumExtension. Pure unit enums throw InvalidArgumentException.
  • find() / findOrDefault() on the class wrapper return native enum cases (not wrappers). Wrap the result with supercharge() when you need instance helpers.
  • Custom enum methods remain available via supercharge($case)->yourMethod() (forwarded with __call).
  • Enums that already use EnumExtension work with supercharge() too; runtime configuration overrides default(), select filtering, and related helpers when set.

Runtime configuration

Configure vendor enums at bootstrap (e.g. in AppServiceProvider::boot()) when you cannot add EnumExtension or need app-specific defaults and select lists:

use BensonDevs\SuperchargedEnums\SuperchargedEnumType;
use function BensonDevs\SuperchargedEnums\supercharge;
use Vendor\Package\VendorStatus;

supercharge(VendorStatus::class)->configureUsing(
    fn (SuperchargedEnumType $type) => $type
        ->setDefault(VendorStatus::Closed)
        ->setSelectables([VendorStatus::Open, 'closed'])
        // ->setUnselectables([...])  // ignored when selectables is set
);

// Anywhere in the app
supercharge(VendorStatus::class)->default(); // VendorStatus::Closed
supercharge(VendorStatus::class)->all();     // only configured selectables
supercharge(VendorStatus::class)->findOrDefault('unknown'); // falls back to configured default

Runtime configuration takes precedence over native enum methods (default(), selectables(), etc.). find() still resolves any declared case — filtering applies to all(), options(), and related select helpers only.

Laravel Eloquent casting

For Laravel models whose attributes use vendor enums, add SuperchargedEnumCast so the attribute is a SuperchargedEnum on read while the database stores the backing scalar.

use BensonDevs\SuperchargedEnums\Laravel\Casts\SuperchargedEnumCast;
use Vendor\Package\OrderStatus;

class Order extends Model
{
    protected function casts(): array
    {
        return [
            // Strict (default) — invalid DB value throws on read
            'status' => SuperchargedEnumCast::of(OrderStatus::class),

            // Lenient — unknown DB value falls back to default case
            'legacy_status' => SuperchargedEnumCast::of(OrderStatus::class, lenient: true),

            // Via supercharge() helper
            'status' => supercharge(OrderStatus::class)->cast(),
        ];
    }
}

$order->status->is('open');
$order->status->unwrap();           // native OrderStatus for type-hinted APIs
$order->toArray()['status'];        // 'open' (backing value, not wrapper)

JSON array columns

For attributes that store arrays of enum backing values in a JSON column:

use BensonDevs\SuperchargedEnums\EnumExtension;
use BensonDevs\SuperchargedEnums\Laravel\Casts\EnumExtensionCollectionCast;
use BensonDevs\SuperchargedEnums\Laravel\Casts\SuperchargedEnumArrayCast;

class Order extends Model
{
    protected function casts(): array
    {
        return [
            // EnumExtension enums — Collection of native cases on read
            'statuses' => EnumExtensionCollectionCast::of(OrderStatus::class),
            'legacy_statuses' => EnumExtensionCollectionCast::of(OrderStatus::class, lenient: true),

            // Vendor / wild enums — array of SuperchargedEnum wrappers on read
            'permissions' => SuperchargedEnumArrayCast::of(VendorPermission::class),
            'legacy_permissions' => SuperchargedEnumArrayCast::of(VendorPermission::class, lenient: true),

            // Via supercharge() helper
            'permissions' => supercharge(VendorPermission::class)->arrayCast(),
        ];
    }
}

$order->statuses[0]->is('open');              // native enum with EnumExtension
$order->permissions[0]->is('read');           // SuperchargedEnum wrapper
$order->toArray()['statuses'];              // ['open', 'closed'] — backing values
Scenario Recommended approach
You own the enum (scalar) use EnumExtension on the enum + Laravel's built-in enum cast
You own the enum (JSON array) EnumExtensionCollectionCast::of(YourEnum::class)
Vendor enum on a model attribute (scalar) SuperchargedEnumCast::of(VendorEnum::class)
Vendor enum on a model attribute (JSON array) SuperchargedEnumArrayCast::of(VendorEnum::class)
Legacy column with bad data lenient: true on the relevant cast

Requires illuminate/database (included in Laravel). Invalid values on assignment always throw; lenient mode applies to reading unknown DB values only.

Modular composition

Individual concerns live under src/Concerns/ and can be used without the full trait:

use BensonDevs\SuperchargedEnums\Concerns\EnumComparisons;
use BensonDevs\SuperchargedEnums\Concerns\EnumLookup;

enum Direction: string
{
    use EnumComparisons;
    use EnumLookup;

    case Left = 'left';
    case Right = 'right';
}

Caveat: findOrDefault() calls default(), which is defined on EnumExtension, not on EnumLookup alone. With a partial stack, use find($key) ?? self::cases()[0] or add your own default() helper.

🔋 Bundled Common enums

Optional backed enums under BensonDevs\SuperchargedEnums\Common\. Each uses EnumExtension. Unless noted in the domain docs, default() is the first declared case; see Core helpers to override.

Per-enum case lists and domain-specific methods are documented under docs/common/.

Domain Documentation Enums
Angle Angle AngleUnit
Application Application DeploymentEnvironment
Calendar Calendar DateDisplayFormat, DayOfWeek, Month, Quarter
Cryptography Cryptography HashAlgorithm
Data size DataSize BinaryDataSizeUnit, DecimalDataSizeUnit
Database Database DatabaseEngine
Device Device DeviceType
Finance Finance CardBrand, MoneyRoundingMode
Geography Geography CompassDirection, Hemisphere
HTTP Http HttpMethod, HttpStatusCode
Identity Identity IdentityDocumentType
Logging Logging LogLevel
Measure Measure AreaUnit, EnergyUnit, FrequencyUnit, LengthUnit, MassUnit, PowerUnit, PressureUnit, SpeedUnit, TemperatureUnit, VolumeUnit
MIME Mime MediaTypeClass
Platform Platform CpuArchitecture, OperatingSystemFamily
Text Text TextCasing, TextTransform
Time Time DurationUnit, Season, TimePrecision

Usage examples

use BensonDevs\SuperchargedEnums\Common\Finance\MoneyRoundingMode;
use BensonDevs\SuperchargedEnums\Common\Measure\LengthUnit;
use BensonDevs\SuperchargedEnums\Common\Time\DurationUnit;

LengthUnit::Mile->toKilometers(1);
DurationUnit::Hour->toSeconds(2);
MoneyRoundingMode::HalfEven->roundMoney(10.005);

Behavior is covered by the Pest test suite.

Development

composer test          # run Pest tests
composer lint          # run Laravel Pint (check only)
composer analyse       # PHPStan
composer type-coverage # PHPStan type coverage (100% enforced)

Support

If this package saves you time, consider supporting its maintenance:

Thank you for helping keep Supercharged Enums maintained.

License

MIT