Added Readme
This commit is contained in:
@@ -0,0 +1,359 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
Reference in New Issue
Block a user