diff --git a/README.md b/README.md index e402331e..c5d6212d 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Table of Contents * [Installation](#installation) * [Usage](#usage) * [Readable enums](#readable-enums) + * [Choice enums](#choice-enums) * [Flagged enums](#flagged-enums) * [Compare](#compare) * [Shortcuts](#shortcuts) @@ -226,6 +227,58 @@ $enum = Gender::get(Gender::MALE); $translator->trans($enum); // returns 'Male' ``` +## Choice enums + +Choice enums are a more opinionated version of readable enums. Using the `ChoiceEnumTrait` in your enum, you'll only +need to implement a `choices()` method instead of both `EnumInterface::values()` and `ReadableEnum::readables()` ones: + +```php + 'Unknown', + self::MALE => 'Male', + self::FEMALE => 'Female', + ]; + } +} +``` + +It is convenient as it implements the two `values` & `readables` methods for you, which means you don't have to keep it in sync anymore. + +The `SimpleChoiceEnum` base class allows you to benefit from both choice enums conveniency along with enumerated values auto-discoverability through public constants: + + +```php +getConstants(); + foreach (self::autodiscoveredValues() as $value) { + $constantName = array_search($value, $constants, true); + self::$guessedReadables[$enumType][$value] = ucfirst(strtolower(str_replace('_', ' ', $constantName))); + } + } + + return self::$guessedReadables[$enumType]; + } } diff --git a/src/ChoiceEnumTrait.php b/src/ChoiceEnumTrait.php new file mode 100644 index 00000000..d7594aae --- /dev/null +++ b/src/ChoiceEnumTrait.php @@ -0,0 +1,76 @@ + + */ + +namespace Elao\Enum; + +use Elao\Enum\Exception\LogicException; + +/** + * Discover readable enumerated values by returning the enumerated values as keys and their labels as values + * in {@link \Elao\Enum\ChoiceEnumTrait::choices()}, replacing the need to provide both: + * - {@link \Elao\Enum\ReadableEnumInterface::readables()} + * - {@link \Elao\Enum\ReadableEnumInterface::values()} + * + * Meant to be used within a {@link \Elao\Enum\ReadableEnumInterface} implementation. + */ +trait ChoiceEnumTrait +{ + /** + * @see EnumInterface::values() + * + * @return int[]|string[] + */ + public static function values(): array + { + self::checkForChoiceEnumTraitMisuses(); + + $values = array_keys(static::choices()); + + if (is_a(static::class, FlaggedEnum::class, true)) { + $values = array_values(array_filter($values, function ($v): bool { + return 0 === ($v & $v - 1); + })); + } + + return $values; + } + + /** + * @see ReadableEnumInterface::readables() + * + * @return string[] labels indexed by enumerated value + */ + public static function readables(): array + { + self::checkForChoiceEnumTraitMisuses(); + + return static::choices(); + } + + /** + * @return string[] The enumerated values as keys and their labels as values. + */ + abstract protected static function choices(): array; + + /** + * @internal + */ + private static function checkForChoiceEnumTraitMisuses() + { + if (!is_a(static::class, ReadableEnumInterface::class, true)) { + throw new LogicException(sprintf( + 'The "%s" trait is meant to be used by "%s" implementations, but "%s" does not implement it.', + ChoiceEnumTrait::class, + ReadableEnumInterface::class, + static::class + )); + } + } +} diff --git a/src/ReadableEnumInterface.php b/src/ReadableEnumInterface.php index 5cb66d43..f675d317 100644 --- a/src/ReadableEnumInterface.php +++ b/src/ReadableEnumInterface.php @@ -17,7 +17,7 @@ interface ReadableEnumInterface extends EnumInterface /** * Gets an array of the human representations indexed by possible values. * - * @return array + * @return string[] labels indexed by enumerated value */ public static function readables(): array; diff --git a/src/SimpleChoiceEnum.php b/src/SimpleChoiceEnum.php new file mode 100644 index 00000000..d9fd6592 --- /dev/null +++ b/src/SimpleChoiceEnum.php @@ -0,0 +1,26 @@ + + */ + +namespace Elao\Enum; + +/** + * An opinionated enum implementation: + * + * - auto-discovers enumerated values from public constants. + * - implements {@link \Elao\Enum\ReadableEnumInterface} with default labels + * identical to enumerated values's constant name. + */ +class SimpleChoiceEnum extends ReadableEnum +{ + use AutoDiscoveredValuesTrait; + use ChoiceEnumTrait { + ChoiceEnumTrait::values insteadof AutoDiscoveredValuesTrait; + } +} diff --git a/tests/Unit/AutoDiscoveredValuesTraitTest.php b/tests/Unit/AutoDiscoveredValuesTraitTest.php index 7d6eb84c..5cdf00e3 100644 --- a/tests/Unit/AutoDiscoveredValuesTraitTest.php +++ b/tests/Unit/AutoDiscoveredValuesTraitTest.php @@ -35,6 +35,15 @@ public function testItAutoDiscoveredValuesBasedOnAvailableBitFlagConstants() { $this->assertSame([1, 2, 4], AutoDiscoveredFlaggedEnum::values()); } + + /** + * @expectedException \Elao\Enum\Exception\LogicException + * @expectedExceptionMessage Method "Elao\Enum\AutoDiscoveredValuesTrait::choices" is only meant to be used when using the "Elao\Enum\ChoiceEnumTrait" trait which is not used in "Elao\Enum\Tests\Unit\AutoDiscoveredEnumMisusingChoices" + */ + public function testThrowsOnChoicesMisuses() + { + AutoDiscoveredEnumMisusingChoices::foo(); + } } final class AutoDiscoveredEnum extends Enum @@ -57,3 +66,15 @@ final class AutoDiscoveredFlaggedEnum extends FlaggedEnum const NOT_A_BIT_FLAG = 3; const NOT_EVEN_AN_INT = 'not_even_an_int'; } + +final class AutoDiscoveredEnumMisusingChoices extends Enum +{ + use AutoDiscoveredValuesTrait; + + const FOO = 'foo'; + + public static function foo() + { + self::choices(); + } +} diff --git a/tests/Unit/ChoiceEnumTraitTest.php b/tests/Unit/ChoiceEnumTraitTest.php new file mode 100644 index 00000000..d628786f --- /dev/null +++ b/tests/Unit/ChoiceEnumTraitTest.php @@ -0,0 +1,112 @@ + + */ + +namespace Elao\Enum\Tests\Unit; + +use Elao\Enum\ChoiceEnumTrait; +use Elao\Enum\Enum; +use Elao\Enum\FlaggedEnum; +use Elao\Enum\ReadableEnum; +use PHPUnit\Framework\TestCase; + +class ChoiceEnumTraitTest extends TestCase +{ + public function testItProvidesValuesAndReadablesImplementations() + { + $this->assertSame(['foo', 'bar', 'baz'], ChoiceEnum::values()); + $this->assertSame([ + ChoiceEnum::FOO => 'Foo label', + ChoiceEnum::BAR => 'Bar label', + ChoiceEnum::BAZ => 'Baz label', + ], ChoiceEnum::readables()); + } + + public function testItFiltersValuesForFlaggedEnumImplementations() + { + $this->assertSame([1, 2, 4], FlaggedEnumWithChoiceEnumTrait::values()); + $this->assertSame([ + FlaggedEnumWithChoiceEnumTrait::EXECUTE => 'Execute', + FlaggedEnumWithChoiceEnumTrait::WRITE => 'Write', + FlaggedEnumWithChoiceEnumTrait::READ => 'Read', + FlaggedEnumWithChoiceEnumTrait::WRITE | FlaggedEnumWithChoiceEnumTrait::READ => 'Read & write', + FlaggedEnumWithChoiceEnumTrait::EXECUTE | FlaggedEnumWithChoiceEnumTrait::READ => 'Read & execute', + FlaggedEnumWithChoiceEnumTrait::ALL => 'All permissions', + ], FlaggedEnumWithChoiceEnumTrait::readables()); + } + + /** + * @expectedException \Elao\Enum\Exception\LogicException + * @expectedExceptionMessage The "Elao\Enum\ChoiceEnumTrait" trait is meant to be used by "Elao\Enum\ReadableEnumInterface" implementations, but "Elao\Enum\Tests\Unit\NonReadableEnumWithChoiceEnumTrait" does not implement it. + */ + public function testValuesThrowsOnNonReadable() + { + NonReadableEnumWithChoiceEnumTrait::values(); + } + + /** + * @expectedException \Elao\Enum\Exception\LogicException + * @expectedExceptionMessage The "Elao\Enum\ChoiceEnumTrait" trait is meant to be used by "Elao\Enum\ReadableEnumInterface" implementations, but "Elao\Enum\Tests\Unit\NonReadableEnumWithChoiceEnumTrait" does not implement it. + */ + public function testReadableThrowsOnNonReadable() + { + NonReadableEnumWithChoiceEnumTrait::readables(); + } +} + +final class ChoiceEnum extends ReadableEnum +{ + use ChoiceEnumTrait; + + const FOO = 'foo'; + const BAR = 'bar'; + const BAZ = 'baz'; + + protected static function choices(): array + { + return [ + static::FOO => 'Foo label', + static::BAR => 'Bar label', + static::BAZ => 'Baz label', + ]; + } +} + +final class FlaggedEnumWithChoiceEnumTrait extends FlaggedEnum +{ + use ChoiceEnumTrait; + + const EXECUTE = 1; + const WRITE = 2; + const READ = 4; + + const ALL = self::EXECUTE | self::WRITE | self::READ; + + public static function choices(): array + { + return [ + static::EXECUTE => 'Execute', + static::WRITE => 'Write', + static::READ => 'Read', + static::WRITE | static::READ => 'Read & write', + static::EXECUTE | static::READ => 'Read & execute', + static::ALL => 'All permissions', + ]; + } +} + +final class NonReadableEnumWithChoiceEnumTrait extends Enum +{ + use ChoiceEnumTrait; + + protected static function choices(): array + { + return ['foo' => 'Foo label']; + } +} diff --git a/tests/Unit/SimpleChoiceEnumTest.php b/tests/Unit/SimpleChoiceEnumTest.php new file mode 100644 index 00000000..64e87cd8 --- /dev/null +++ b/tests/Unit/SimpleChoiceEnumTest.php @@ -0,0 +1,54 @@ + + */ + +namespace Elao\Enum\Tests\Unit; + +use Elao\Enum\SimpleChoiceEnum; +use PHPUnit\Framework\TestCase; + +class SimpleChoiceEnumTest extends TestCase +{ + public function testSimpleChoiceEnum() + { + $this->assertSame(['foo', 'bar', 'baz'], DummySimpleChoiceEnum::values()); + $this->assertSame([ + ChoiceEnum::FOO => 'Foo', + ChoiceEnum::BAR => 'Bar', + ChoiceEnum::BAZ => 'Baz', + ], DummySimpleChoiceEnum::readables()); + } + + public function testSimpleChoiceEnumWithLabelOverride() + { + $this->assertSame(['foo', 'bar', 'baz'], DummySimpleChoiceEnumWithLabelOverride::values()); + $this->assertSame([ + ChoiceEnum::FOO => 'Foo label', + ChoiceEnum::BAR => 'Bar', + ChoiceEnum::BAZ => 'Baz', + ], DummySimpleChoiceEnumWithLabelOverride::readables()); + } +} + +class DummySimpleChoiceEnum extends SimpleChoiceEnum +{ + const FOO = 'foo'; + const BAR = 'bar'; + const BAZ = 'baz'; +} + +final class DummySimpleChoiceEnumWithLabelOverride extends DummySimpleChoiceEnum +{ + protected static function choices(): array + { + return array_replace(parent::choices(), [ + static::FOO => 'Foo label', + ]); + } +}