diff --git a/README.md b/README.md index 4a54fe0..e8eaaec 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ A collection of enum helpers for PHP. - [`Names`](#names) - [`Values`](#values) - [`Options`](#options) +- [`From`](#from) You can read more about the idea on [Twitter](https://twitter.com/archtechx/status/1495158228757270528). I originally wanted to include the `InvokableCases` helper in [`archtechx/helpers`](https://github.com/archtechx/helpers), but it makes more sense to make it a separate dependency and use it *inside* the other package. @@ -144,6 +145,65 @@ enum TaskStatus: int TaskStatus::options(); // ['INCOMPLETE' => 0, 'COMPLETED' => 1, 'CANCELED' => 2] ``` +### From + +This helper adds `from()` and `tryFrom()` to pure enums, and adds `fromName()` and `tryFromName()` to all enums. + +#### Important Notes: +* `BackedEnum` instances already implement their own `from()` and `tryFrom()` methods, which will not be overridden by this trait. Attempting to override those methods in a `BackedEnum` causes a fatal error. +* Pure enums do not actually have an internal index, so rearranging the order of cases will not break other code but it will create inconsistent behaviour with the `from()` and `tryFrom()` methods + +#### Apply the trait on your enum +```php +use ArchTech\Enums\From; + +enum TaskStatus: int +{ + use From; + + case INCOMPLETE = 0; + case COMPLETED = 1; + case CANCELED = 2; +} + +enum Role +{ + use From; + + case ADMINISTRATOR; + case SUBSCRIBER; + case GUEST; +} +``` + +#### Use the `from()` method +```php +Role::from(0); // Role::ADMINISTRATOR +Role::from(5); // Error: \ValueError +``` + +#### Use the `tryFrom()` method +```php +Role::tryFrom(2); // Role::GUEST +Role::tryFrom(5); // null +``` + +#### Use the `fromName()` method +```php +TaskStatus::fromName('INCOMPLETE'); // TaskStatus::INCOMPLETE +Role::fromName('SUBSCRIBER'); // Role::SUBSCRIBER +TaskStatus::fromName('MISSING'); // Error: \ValueError +Role::fromName('HACKER'); // Error: \ValueError +``` + +#### Use the `tryFromName()` method +```php +TaskStatus::tryFromName('COMPLETED'); // TaskStatus::COMPLETED +TaskStatus::tryFromName('NOTHING'); // null +Role::tryFromName('GUEST'); // Role::GUEST +Role::tryFromName('TESTER'); // null +``` + ## Development Run all checks locally: diff --git a/src/From.php b/src/From.php new file mode 100644 index 0000000..849780a --- /dev/null +++ b/src/From.php @@ -0,0 +1,53 @@ + $c->name === $case + ); + + return array_pop($cases) ?? null; + } +} diff --git a/tests/Pest.php b/tests/Pest.php index 35b60c5..a80cbb7 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -11,6 +11,7 @@ | */ +use ArchTech\Enums\From; use ArchTech\Enums\InvokableCases; use ArchTech\Enums\Names; use ArchTech\Enums\Options; @@ -51,8 +52,16 @@ function something() enum Status: int { - use InvokableCases, Options, Names, Values; + use InvokableCases, Options, Names, Values, From; case PENDING = 0; case DONE = 1; } + +enum Role +{ + use InvokableCases, Options, Names, Values, From; + + case ADMIN; + case GUEST; +} diff --git a/tests/Pest/FromTest.php b/tests/Pest/FromTest.php new file mode 100644 index 0000000..49f07eb --- /dev/null +++ b/tests/Pest/FromTest.php @@ -0,0 +1,65 @@ +expect(Status::from(0)) + ->toBe(Status::PENDING); + +it('does not override the default BackedEnum from method with errors', function () { + Status::from(2); +})->throws(\ValueError::class, '2 is not a valid backing value for enum "Status"'); + +it('does not override the default BackedEnum tryFrom method') + ->expect(Status::tryFrom(1)) + ->toBe(Status::DONE); + +it('does not override the default BackedEnum tryFrom method with errors') + ->expect(Status::tryFrom(2)) + ->toBe(null); + +it('can select a case by index with from() for pure enums') + ->expect(Role::from(0)) + ->toBe(Role::ADMIN); + +it('throws a value error when selecting a non-existent case by index with from() for pure enums', function () { + Role::from(2); +})->throws(\ValueError::class, '2 is not a valid unit for enum "Role"'); + +it('can select a case by index with tryFrom() for pure enums') + ->expect(Role::tryFrom(1)) + ->toBe(Role::GUEST); + +it('can returns null when selecting a non-existent case by index with tryFrom() for pure enums') + ->expect(Role::tryFrom(2)) + ->toBe(null); + +it('can select a case by name with fromName() for pure enums') + ->expect(Role::fromName('ADMIN')) + ->toBe(Role::ADMIN); + +it('throws a value error when selecting a non-existent case by name with fromName() for pure enums', function () { + Role::fromName('NOTHING'); +})->throws(\ValueError::class, '"NOTHING" is not a valid name for enum "Role"'); + +it('can select a case by name with tryFromName() for pure enums') + ->expect(Role::tryFromName('GUEST')) + ->toBe(Role::GUEST); + +it('returns null when selecting a non-existent case by name with tryFromName() for pure enums') + ->expect(Role::tryFromName('NOTHING')) + ->toBe(null); + +it('can select a case by name with fromName() for backed enums') + ->expect(Status::fromName('PENDING')) + ->toBe(Status::PENDING); + +it('throws a value error when selecting a non-existent case by name with fromName() for backed enums', function () { + Status::fromName('NOTHING'); +})->throws(\ValueError::class, '"NOTHING" is not a valid name for enum "Status"'); + +it('can select a case by name with tryFromName() for backed enums') + ->expect(Status::tryFromName('DONE')) + ->toBe(Status::DONE); + +it('returns null when selecting a non-existent case by name with tryFromName() for backed enums') + ->expect(Status::tryFromName('NOTHING')) + ->toBe(null);