From 65072ec3d77cb2de2b429d7306ba7a940f9c8f55 Mon Sep 17 00:00:00 2001 From: l0gic Date: Fri, 5 Jun 2026 23:20:43 +0500 Subject: [PATCH 1/2] feat: add fromName()/tryFromName() to resolve a case by name --- README.md | 10 +++- src/ExtendedBackedEnum.php | 25 +++++++++ src/ExtendedBackedEnumInterface.php | 8 +++ tests/ExtendedBackedEnum/FromNameTest.php | 58 ++++++++++++++++++++ tests/ExtendedBackedEnum/TryFromNameTest.php | 57 +++++++++++++++++++ 5 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 tests/ExtendedBackedEnum/FromNameTest.php create mode 100644 tests/ExtendedBackedEnum/TryFromNameTest.php diff --git a/README.md b/README.md index d2b23a5..f9a987e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ composer require k2gl/enum ## Usage ```php -use K2gl\Enum\src\ExtendedBackedEnumInterface;use K2gl\Enum\Types\ExtendedBackedEnum\ExtendedBackedEnum; +use K2gl\Enum\ExtendedBackedEnum; +use K2gl\Enum\ExtendedBackedEnumInterface; enum CardSuit: string implements ExtendedBackedEnumInterface { @@ -54,6 +55,13 @@ $suit->isNot(ResponseCode::HTTP_I_AM_A_TEAPOT); // false CardSuit::names(); // ['HEARTS', 'DIAMONDS', 'CLUBS', 'SPADES'] CardSuit::values(); // ['hearts', 'diamonds', 'clubs', 'spades'] + +// Resolve a case by its name — the counterpart of the native from()/tryFrom(), +// which only resolve by backing value. +CardSuit::fromName('SPADES'); // CardSuit::SPADES +CardSuit::tryFromName('SPADES'); // CardSuit::SPADES +CardSuit::tryFromName('joker'); // null +CardSuit::fromName('joker'); // throws \ValueError ``` ## Pull requests are always welcome diff --git a/src/ExtendedBackedEnum.php b/src/ExtendedBackedEnum.php index 29b46c0..54eb141 100644 --- a/src/ExtendedBackedEnum.php +++ b/src/ExtendedBackedEnum.php @@ -44,6 +44,31 @@ public static function anyoneExcept(BackedEnum|array $except): static return self::from($values[\array_rand($values)]); } + /** + * Resolve a case by its name, mirroring the native from() which only + * resolves by backing value. + * + * @throws ValueError when no case has the given name + */ + public static function fromName(string $name): static + { + return self::tryFromName($name) + ?? throw new ValueError( + sprintf('"%s" is not a valid name for enum %s', $name, self::class) + ); + } + + public static function tryFromName(string $name): ?static + { + foreach (self::cases() as $case) { + if ($case->name === $name) { + return $case; + } + } + + return null; + } + public function is(mixed $value): bool { if ($value instanceof BackedEnum) { diff --git a/src/ExtendedBackedEnumInterface.php b/src/ExtendedBackedEnumInterface.php index f21c8d9..8ca4658 100644 --- a/src/ExtendedBackedEnumInterface.php +++ b/src/ExtendedBackedEnumInterface.php @@ -5,6 +5,7 @@ namespace K2gl\Enum; use BackedEnum; +use ValueError; interface ExtendedBackedEnumInterface extends BackedEnum { @@ -15,6 +16,13 @@ public static function any(): static; */ public static function anyoneExcept(BackedEnum|array $except): static; + /** + * @throws ValueError when no case has the given name + */ + public static function fromName(string $name): static; + + public static function tryFromName(string $name): ?static; + public function is(mixed $value): bool; public function isNot(mixed $value): bool; diff --git a/tests/ExtendedBackedEnum/FromNameTest.php b/tests/ExtendedBackedEnum/FromNameTest.php new file mode 100644 index 0000000..b402bfc --- /dev/null +++ b/tests/ExtendedBackedEnum/FromNameTest.php @@ -0,0 +1,58 @@ +is($expected); + } + + #[DataProvider('unknownNameDataProvider')] + public function testThrowsOnUnknownName(string $enumClass, string $name): void + { + // assert + $this->expectException(ValueError::class); + + // act + $enumClass::fromName($name); + } + + public static function validNameDataProvider(): array + { + return [ + ['HEARTS', CardSuit::HEARTS], + ['SPADES', CardSuit::SPADES], + ['HTTP_OK', ResponseCode::HTTP_OK], + ['HTTP_I_AM_A_TEAPOT', ResponseCode::HTTP_I_AM_A_TEAPOT], + ]; + } + + public static function unknownNameDataProvider(): array + { + return [ + 'missing name' => [CardSuit::class, 'JOKER'], + 'wrong case' => [CardSuit::class, 'hearts'], + 'backing value' => [CardSuit::class, 'spades'], + 'empty string' => [CardSuit::class, ''], + 'int enum, missing' => [ResponseCode::class, 'HTTP_TEAPOT'], + ]; + } +} diff --git a/tests/ExtendedBackedEnum/TryFromNameTest.php b/tests/ExtendedBackedEnum/TryFromNameTest.php new file mode 100644 index 0000000..f0157da --- /dev/null +++ b/tests/ExtendedBackedEnum/TryFromNameTest.php @@ -0,0 +1,57 @@ +is($expected); + } + + #[DataProvider('unknownNameDataProvider')] + public function testReturnsNullOnUnknownName(string $enumClass, string $name): void + { + // act + $case = $enumClass::tryFromName($name); + + // assert + fact($case)->null(); + } + + public static function validNameDataProvider(): array + { + return [ + ['HEARTS', CardSuit::HEARTS], + ['SPADES', CardSuit::SPADES], + ['HTTP_OK', ResponseCode::HTTP_OK], + ['HTTP_I_AM_A_TEAPOT', ResponseCode::HTTP_I_AM_A_TEAPOT], + ]; + } + + public static function unknownNameDataProvider(): array + { + return [ + 'missing name' => [CardSuit::class, 'JOKER'], + 'wrong case' => [CardSuit::class, 'hearts'], + 'backing value' => [CardSuit::class, 'spades'], + 'empty string' => [CardSuit::class, ''], + 'int enum, missing' => [ResponseCode::class, 'HTTP_TEAPOT'], + ]; + } +} From 64c2d7676f966fcf1a307eec85c33f8a251bf623 Mon Sep 17 00:00:00 2001 From: l0gic Date: Fri, 5 Jun 2026 23:20:43 +0500 Subject: [PATCH 2/2] chore: raise PHPStan to level 9 --- phpstan.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index 3b5158e..9ec7a73 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 6 + level: 9 paths: - src - tests/Examples