From cc5bba1912513699b19fd6563073169402e82bb1 Mon Sep 17 00:00:00 2001 From: Samuel Levy Date: Thu, 24 Mar 2022 07:18:58 +1000 Subject: [PATCH] Issue #4: Added `From` trait (#7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #4 Added `From` trait This adds the `from()` and `tryFrom()` methods to pure enums, and the `fromName()` and `tryFromName()` methods for all enum types. * Code review changes Dropped support for fragile indexes on pure enums * Dropped unnecessary array_pop() * import ValueError * remove import Co-authored-by: Samuel Štancl Co-authored-by: Samuel Štancl --- README.md | 60 +++++++++++++++++++++++++++++++++++++ src/From.php | 55 ++++++++++++++++++++++++++++++++++ tests/Pest.php | 5 ++-- tests/Pest/FromTest.php | 65 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 src/From.php create mode 100644 tests/Pest/FromTest.php diff --git a/README.md b/README.md index 93deb8d..15cee4a 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. @@ -186,6 +187,65 @@ TaskStatus::options(); // ['INCOMPLETE' => 0, 'COMPLETED' => 1, 'CANCELED' => 2] Role::options(); // ['ADMINISTRATOR', 'SUBSCRIBER', 'GUEST'] ``` +### 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 only have named cases and not values, so the `from()` and `tryFrom()` methods are functionally equivalent to `fromName()` and `tryFromName()` + +#### 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('ADMINISTRATOR'); // Role::ADMINISTRATOR +Role::from('NOBODY'); // Error: ValueError +``` + +#### Use the `tryFrom()` method +```php +Role::tryFrom('GUEST'); // Role::GUEST +Role::tryFrom('NEVER'); // null +``` + +#### Use the `fromName()` method +```php +TaskStatus::fromName('INCOMPLETE'); // TaskStatus::INCOMPLETE +TaskStatus::fromName('MISSING'); // Error: ValueError +Role::fromName('SUBSCRIBER'); // Role::SUBSCRIBER +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..916b12f --- /dev/null +++ b/src/From.php @@ -0,0 +1,55 @@ + $c->name === $case + ); + + return array_values($cases)[0] ?? null; + } +} diff --git a/tests/Pest.php b/tests/Pest.php index 4e535cc..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,7 +52,7 @@ function something() enum Status: int { - use InvokableCases, Options, Names, Values; + use InvokableCases, Options, Names, Values, From; case PENDING = 0; case DONE = 1; @@ -59,7 +60,7 @@ enum Status: int enum Role { - use InvokableCases, Options, Names, Values; + 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..9ec5ce3 --- /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 name with from() for pure enums') + ->expect(Role::from('ADMIN')) + ->toBe(Role::ADMIN); + +it('throws a value error when selecting a non-existent case with from() for pure enums', function () { + Role::from('NOBODY'); +})->throws(ValueError::class, '"NOBODY" is not a valid name for enum "Role"'); + +it('can select a case by name with tryFrom() for pure enums') + ->expect(Role::tryFrom('GUEST')) + ->toBe(Role::GUEST); + +it('can returns null when selecting a non-existent case by name with tryFrom() for pure enums') + ->expect(Role::tryFrom('NOBODY')) + ->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('NOBODY'); +})->throws(ValueError::class, '"NOBODY" 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('NOBODY')) + ->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);