# tez-utils-enum Composable PHP 8.3 traits that extend native enums with lookup, comparison, navigation, bitmask, label, and collection helpers. **Zero dependencies** · **PHPStan level 9** · **MIT** ## Installation ```bash composer require tez/utils-enum ``` Requires PHP ≥ 8.3. ## Traits | Trait | Enum type | Purpose | |---|---|---| | `EnumTrait` | Unit | Base helpers: lookup, filtering, ordering | | `BackedEnumTrait` | Backed | Extends `EnumTrait` with value-based helpers | | `BitFlagTrait` | Int-backed | Bitmask operations | | `ComparableTrait` | Int-backed | Comparison and range checks | | `StepTrait` | Any | Navigate to adjacent cases | | `TranslatableTrait` | Any | Human-readable labels | | `EnumCollectionTrait` | Any | Collection-style selection | --- ## EnumTrait Base trait for pure (unit) enums. Use `BackedEnumTrait` instead when your enum has backing values. ```php use Tez\Utils\Enum\EnumTrait; enum Status { use EnumTrait; case Draft; case Published; case Archived; } Status::find('Published'); // Status::Published Status::find('published', ignoreCase: true); // Status::Published Status::Published->is('Published'); // true Status::Published->is(Status::Published); // true Status::Published->isOneOf([Status::Draft, Status::Published]); // true Status::Published->isNoneOf([Status::Archived]); // true Status::Published->ordinal(); // 1 Status::first(); // Status::Draft Status::last(); // Status::Archived Status::filter(fn($c) => $c !== Status::Archived); // [Draft, Published] Status::toArray(); // ['Draft' => 'Draft', 'Published' => 'Published', 'Archived' => 'Archived'] ``` ### Methods | Method | Description | |---|---| | `is($value)` | True if the case matches by identity or name. Null always returns false. | | `isOneOf(?array $values)` | True if the case matches any value in the list. | | `isNoneOf(?array $values)` | Inverse of `isOneOf()`. | | `find($value, $ignoreCase)` | Returns a matching case or null. Matches by identity or name. | | `filter(callable $cb)` | Returns all cases for which the callback returns true. | | `toArray()` | Returns `[name => name]` for all cases. | | `ordinal()` | Zero-based position of this case in the enum definition. | | `first()` | Returns the first defined case. | | `last()` | Returns the last defined case. | --- ## BackedEnumTrait Extends `EnumTrait` for string- or int-backed enums. Overrides `is()` to match by backing value, and `toArray()` to return `[value => name]`. ```php use Tez\Utils\Enum\BackedEnumTrait; enum Color: string { use BackedEnumTrait; case Red = 'red'; case Green = 'green'; case Blue = 'blue'; } Color::find('red'); // Color::Red — matched by value Color::find('Red'); // Color::Red — matched by name Color::find('RED', ignoreCase: true); // Color::Red Color::hasValue('blue'); // true Color::values(); // ['red', 'green', 'blue'] Color::fromName('Green'); // Color::Green (throws ValueError if not found) Color::tryFromName('X'); // null Color::toArray(); // ['red' => 'Red', 'green' => 'Green', 'blue' => 'Blue'] Color::toValueArray(); // ['Red' => 'red', 'Green' => 'green', 'Blue' => 'blue'] ``` > **`toArray()` vs `toValueArray()`** — `toArray()` returns `[value => name]`, useful for form selects. > `toValueArray()` returns `[name => value]`, useful for serialization. The directions are opposite. ### Additional methods | Method | Description | |---|---| | `values()` | All backing values as a list. | | `hasValue($value)` | True if any case has this backing value. | | `fromName(string $name)` | Returns a case by name, or throws `ValueError`. | | `tryFromName(string $name)` | Returns a case by name, or null. | | `toValueArray()` | Returns `[name => value]` for all cases. | --- ## BitFlagTrait Bitmask helpers for int-backed enums used as flag sets. Case values must be powers of 2 (`1, 2, 4, 8, …`). This constraint is not enforced at runtime. Does not include `BackedEnumTrait` — combine them explicitly if needed. ```php use Tez\Utils\Enum\BitFlagTrait; enum Permission: int { use BitFlagTrait; case Read = 1; case Write = 2; case Execute = 4; } // Build a mask $mask = Permission::combine(Permission::Read, Permission::Write); // 3 // Check flags Permission::Read->isSetIn($mask); // true Permission::Execute->isSetIn($mask); // false // Add / remove flags $mask = Permission::Execute->addTo($mask); // 7 $mask = Permission::Write->removeFrom($mask); // 5 // Expand a mask back to cases Permission::fromBitmask($mask); // [Permission::Read, Permission::Execute] Permission::fromBitmask(0); // [] ``` ### Methods | Method | Description | |---|---| | `combine(self ...$cases)` | OR-combines case values into a bitmask. Returns 0 with no arguments. | | `fromBitmask(int $mask)` | Returns all cases whose bit is set in the mask. Skips cases with value 0. | | `isSetIn(int $mask)` | True if this case's bit is set in the mask. | | `addTo(int $mask)` | Returns the mask with this case's bit set. | | `removeFrom(int $mask)` | Returns the mask with this case's bit cleared. | --- ## ComparableTrait Comparison helpers for int-backed enums, comparing cases by their backing values. ```php use Tez\Utils\Enum\ComparableTrait; enum Priority: int { use ComparableTrait; case Low = 1; case Medium = 2; case High = 3; } Priority::High->isGreaterThan(Priority::Low); // true Priority::Medium->isGreaterThanOrEqual(Priority::Medium); // true Priority::Medium->between(Priority::Low, Priority::High); // true Priority::High->clamp(Priority::Low, Priority::Medium); // Priority::Medium ``` ### Methods | Method | Description | |---|---| | `isGreaterThan(self $other)` | True if this case's value is strictly greater. | | `isGreaterThanOrEqual(self $other)` | True if this case's value is greater or equal. | | `isLessThan(self $other)` | True if this case's value is strictly less. | | `isLessThanOrEqual(self $other)` | True if this case's value is less or equal. | | `between($min, $max)` | True if within the inclusive range `[$min, $max]`. | | `clamp($min, $max)` | Returns this case, or `$min`/`$max` if out of range. | --- ## StepTrait Navigate to adjacent cases by definition order. Designed for state machines and ordered workflows. Works with both unit and backed enums. Order is determined by **definition order** (ordinal), not backing value. Returns `null` at boundaries — no wrap-around. ```php use Tez\Utils\Enum\StepTrait; enum CheckoutStep { use StepTrait; case PersonalInfo; case Address; case Payment; case Confirmation; } CheckoutStep::Address->next(); // CheckoutStep::Payment CheckoutStep::Address->prev(); // CheckoutStep::PersonalInfo CheckoutStep::Confirmation->next(); // null (last step) CheckoutStep::PersonalInfo->prev(); // null (first step) ``` ### Methods | Method | Description | |---|---| | `next()` | Returns the next case by definition order, or null at the last case. | | `prev()` | Returns the previous case by definition order, or null at the first case. | --- ## TranslatableTrait Human-readable labels for enum cases, without any framework dependency. Override `labels()` to supply custom mappings keyed by case name. Falls back to the case name when no label is defined. ```php use Tez\Utils\Enum\TranslatableTrait; enum UserRole { use TranslatableTrait; case Admin; case Editor; case Viewer; protected static function labels(): array { return [ self::Admin->name => 'Administrator', self::Editor->name => 'Content Editor', // Viewer has no entry — falls back to 'Viewer' ]; } } UserRole::Admin->label(); // 'Administrator' UserRole::Viewer->label(); // 'Viewer' (fallback) UserRole::labelsArray(); // ['Admin' => 'Administrator', 'Editor' => 'Content Editor', 'Viewer' => 'Viewer'] ``` ### Methods | Method | Description | |---|---| | `label()` | Human-readable label for this case. Falls back to `$case->name`. | | `labelsArray()` | All cases mapped to their labels as `[caseName => label]`. | | `labels()` *(protected)* | Override in your enum to supply custom label mappings. | --- ## EnumCollectionTrait Collection-style selection helpers. Can be combined with `EnumTrait` or `BackedEnumTrait`. Works with any enum type. ```php use Tez\Utils\Enum\EnumCollectionTrait; enum Suit { use EnumCollectionTrait; case Hearts; case Diamonds; case Clubs; case Spades; } Suit::except(Suit::Hearts, Suit::Diamonds); // [Suit::Clubs, Suit::Spades] Suit::only(Suit::Clubs, Suit::Spades); // [Suit::Clubs, Suit::Spades] Suit::random(); // a random Suit case ``` Both `except()` and `only()` preserve enum definition order. ### Methods | Method | Description | |---|---| | `random()` | Returns a random case. | | `except(self ...$excluded)` | All cases except the given ones, in definition order. | | `only(self ...$included)` | Only the given cases, in definition order. | --- ## Combining traits Traits are designed to be freely mixed. A few common combinations: ```php // Full-featured backed enum enum Status: string { use BackedEnumTrait, TranslatableTrait, EnumCollectionTrait, StepTrait; case Draft = 'draft'; case Review = 'review'; case Published = 'published'; protected static function labels(): array { return [ self::Draft->name => 'Draft', self::Review->name => 'In Review', self::Published->name => 'Published', ]; } } Status::Draft->next(); // Status::Review Status::Review->label(); // 'In Review' Status::except(Status::Draft); // [Status::Review, Status::Published] Status::find('draft'); // Status::Draft ``` ```php // Int-backed enum with bitmask + comparison enum Permission: int { use BackedEnumTrait, BitFlagTrait, ComparableTrait; case Read = 1; case Write = 2; case Execute = 4; } ``` > All traits use `@phpstan-require-implements` to enforce the correct enum type at static analysis time. > Mixing `BitFlagTrait` or `ComparableTrait` with non-int-backed enums will surface as a PHPStan error. ## Requirements - PHP 8.3+ - No runtime dependencies ## License MIT