Added Readme

This commit is contained in:
René Halberstadt
2026-07-18 15:33:47 +02:00
parent 1d97f9aab1
commit 3b69452a43
+359
View File
@@ -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