From 40b445d44cd1f6cdc1500d2cef88f6b9abc8ed2e Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Sun, 8 Apr 2018 17:29:11 +0200 Subject: [PATCH] Add ChoiceEnumTrait & SimpleChoiceEnum --- src/AutoDiscoveredValuesTrait.php | 30 +++++ src/ChoiceEnumTrait.php | 76 +++++++++++++ src/ReadableEnumInterface.php | 2 +- src/SimpleChoiceEnum.php | 25 +++++ tests/Unit/AutoDiscoveredValuesTraitTest.php | 21 ++++ tests/Unit/ChoiceEnumTraitTest.php | 112 +++++++++++++++++++ tests/Unit/SimpleChoiceEnumTest.php | 54 +++++++++ 7 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 src/ChoiceEnumTrait.php create mode 100644 src/SimpleChoiceEnum.php create mode 100644 tests/Unit/ChoiceEnumTraitTest.php create mode 100644 tests/Unit/SimpleChoiceEnumTest.php diff --git a/src/AutoDiscoveredValuesTrait.php b/src/AutoDiscoveredValuesTrait.php index 5e5b39b5..504b31d6 100644 --- a/src/AutoDiscoveredValuesTrait.php +++ b/src/AutoDiscoveredValuesTrait.php @@ -10,6 +10,11 @@ namespace Elao\Enum; +use Elao\Enum\Exception\LogicException; + +/** + * Auto-discover enumerated values from public constants. + */ trait AutoDiscoveredValuesTrait { /** @var array */ @@ -21,6 +26,31 @@ trait AutoDiscoveredValuesTrait * @return int[]|string[] */ public static function values(): array + { + return static::autodiscoveredValues(); + } + + /** + * @see ChoiceEnumTrait::choices() + */ + protected static function choices(): array + { + if (!in_array(ChoiceEnumTrait::class, class_uses(self::class, false), true)) { + throw new LogicException(sprintf( + 'Method "%s" is only meant to be used when using the "%s" trait which is not used in "%s"', + __METHOD__, + ChoiceEnumTrait::class, + static::class + )); + } + + return array_combine(static::autodiscoveredValues(), static::autodiscoveredValues()); + } + + /** + * @internal + */ + private static function autodiscoveredValues(): array { $enumType = static::class; diff --git a/src/ChoiceEnumTrait.php b/src/ChoiceEnumTrait.php new file mode 100644 index 00000000..ac518ff0 --- /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 + { + static::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 + { + static::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..7b389ba1 --- /dev/null +++ b/src/SimpleChoiceEnum.php @@ -0,0 +1,25 @@ + + */ + +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. + */ +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..50d2371b --- /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 => ChoiceEnum::FOO, + ChoiceEnum::BAR => ChoiceEnum::BAR, + ChoiceEnum::BAZ => ChoiceEnum::BAZ, + ], DummySimpleChoiceEnum::readables()); + } + + public function testSimpleChoiceEnumWithLabelOverride() + { + $this->assertSame(['foo', 'bar', 'baz'], DummySimpleChoiceEnumWithLabelOverride::values()); + $this->assertSame([ + ChoiceEnum::FOO => 'Foo label', + ChoiceEnum::BAR => ChoiceEnum::BAR, + ChoiceEnum::BAZ => ChoiceEnum::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', + ]); + } +}