diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a390c..4e447b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,31 @@ -# [Unreleased] +# 2.0.0 + +This release is a significant overhaul of the existing API, and therefore introduces breaking changes. +See the list of updates below, and consult the [README](./README.md) for examples and details of the new API. - Add Enum::instanceFromKey($key) +- **Breaking** Change `$instance->identifier` to `$instance->name()` +- **Breaking** Change `Enum::identifiers()` to `Enum::names()` +- **Breaking** Change `Enum::getKeyForIdentfier()` to `Enum::keyForName()` +- **Breaking** Change `Enum::valueFor()` to `Enum::valueForKey()` +- Add `Enum::nameForKey()` to get the constant name for a given key +- **Breaking** Change `Enum::exists()` to `Enum::isValidKey()` +- **Breaking** Change `Enum::checkExists()` to `Enum::requireValidKey()` +- Fix `$instance->key()` to handle non-string keys +- Fix `$instance->is()` to handle non-string keys +- Fix late-static binding in some methods which referred to `self::` +- Add `Enum::instanceFromName($name)` to get an instance via name (alternative to Enum::NAME()) +- Change implementation of `Enum::instanceFromKey($key)` to use array_search +- **Breaking** Change: the default provided static `map()` method will return an array of constant keys mapped to `null`. +Previously it returned an empty array `[]` when not overridden. In practice, this may not effect userland code. +- **Breaking** Change: you can no longer provide a non-keyed array in an `map()` method implemented +in your sub-class. This method should be used to map keys to values (if necessary). A default map() method is provided +which maps keys to `null` values. +- **Breaking** Change `Enum::fromValue($val)` has been renamed to `Enum::keyForValue()` +- **Breaking** Change: removed `Enum::flip()` +- **Breaking** Change `Enum::constantMap()` to `Enum::namesAndKeys()` +- Updated README to reflect API changes +- Add `Enum::valueForName($name)` for completeness # 1.1.0 diff --git a/README.md b/README.md index b9b963e..8a13dec 100644 --- a/README.md +++ b/README.md @@ -13,72 +13,67 @@ This library provides an Enum / Enumeration implementation for PHP. ## Why use this library * Very simple to implement and use. -* Flexible values can be assigned. +* Complex can optionally be mapped by providing a `map()` method. * Allows type-hinting when passing enumerated values between methods and classes. ## Usage -### Extend the Enum class -Extending the `Enum` class is simple; +First create a new class that extends `\Rexlabs\Enum\Enum` and do the following; -1. Simply declare any constants +1. Declare your constants 2. *Optional*: provide a `map()` method: +## Example + ```php optional value - public static function map(): array + const BRISBANE = 'Brisbane'; + const MELBOURNE = 'Melbourne'; + const SYDNEY = 'Sydney'; + + // OPTIONAL - Provide a map() method if you would like to + // map additional data, which will be available from the ->value() method + public static function map(): array { return [ - self::CAT => 'Kitty-cat', - self::DOG => null, - self::HORSE => null, - self::PIGEON => ['you','filthy','animal'], + static::BRISBANE => ["state"=>"QLD", "population"=>""], + static::MELBOURNE => ["state"=>"VIC", "population"=>"5m"], + static::SYDNEY => ["state"=>"NSW", "population"=>"5m"], ]; } + } -``` - -### Use your extended class - -```php -identifier(); // "CAT" -$cat->key(); // "kitty" -$cat->value(); // "Kitty-cat" -$cat->is(Animal::CAT); // (boolean)true -$cat->is(Animal::CAT()); // (boolean)true -$cat->is(Animal::PIGEON()); // (boolean)false -Animal::DOG()->value(); // (null) - No value was assigned in map() - -Animal::PIGEON()->key(); // "skyrat" -Animal::PIGEON()->value(); // (array)['you', 'filthy', 'animal'] - -// Get a new Enum instance from a given key -$kitty = 'kitty'; -$cat = Animal::instanceFromKey($kitty); +// Static access +echo City::BRISBANE; // "Brisbane" +echo City::MELBOURNE; // "Melbourne" +City::names(); // (array)["BRISBANE", "BRISBANE", "SYDNEY"] +City::keys(); // (array)["Brisbane", "Melbourne", "Sydney"] +City::keyForName('BRISBANE'); // "Brisbane" +City::nameForKey('Melbourne'); // "MELBOURNE" +City::isValidKey('Sydney'); // (boolean)true +City::isValidKey('Paris'); // (boolean)false + +// Getting an instance - all return a City instance. +$city = City::BRISBANE(); +$city = City::instanceFromName('BRISBANE'); +$city = City::instanceFromKey('Brisbane'); + +// Working with an instance +$city->name(); // "BRISBANE" +$city->key(); // "Brisbane" +$city->value()['population']; // null - no value mapped +$city->is(City::BRISBANE); // (boolean)true +$city->is(City::BRISBANE()); // (boolean)true +$city->is(City::SYDNEY()); // (boolean)false + +// Or ... +City::SYDNEY()->key(); // "Sydney" +City::SYDNEY()->value(); // (array)["state"=>"NSW", "population"=>"5m"] ``` ## Dependencies @@ -93,30 +88,18 @@ To install in your project: composer require rexlabs/enum ``` -## More Examples - - - ### Type-hinting Now you can type-hint your `Enum` object as a dependency: ```php value() ?? $animal->key(); - if (is_array($name)) { - $name = implode(' ', $name); - } - - echo "Greeting for {$animal->identifier()}: Hello $name\n"; - +function announceCity(City $city) { + echo "{$city->key()} is located in {$city->value()["state"]}, population: {$city->value()["population"]}\n"; } // Get a new instance -sayHelloTo(Animal::CAT()); // "Greeting for CAT: Hello Kitty-cat" -sayHelloTo(Animal::DOG()); // "Greeting for DOG: Hello dog" -sayHelloTo(Animal::PIGEON()); // "Greeting for PIGEON: Hello you filthy animal" +announceCity(City::SYDNEY()); // "Sydney is located in NSW, population: 5m" ``` @@ -124,12 +107,12 @@ sayHelloTo(Animal::PIGEON()); // "Greeting for PIGEON: Hello you filthy animal Each instance of `Enum` provides the following methods: -### identifier() +### name() -Returns the constant identifier. +Returns the constant name. ```php -$enum->identifier(); +$enum->name(); ``` ### key() @@ -151,19 +134,19 @@ $enum->value(); ### is(Enum|string $compare) -Returns true if this instance is the same as the given constant string or enumeration instance. +Returns true if this instance is the same as the given constant key or enumeration instance. ```php -$enum->is(Animal::CAT); // Compare to constant key -$enum->is(Animal::CAT()); // Compare to instance +$enum->is(City::SYDNEY); // Compare to constant key +$enum->is(City::SYDNEY()); // Compare to instance ``` ### __toString() -The `__toString()` method is defined to return the instance identifier (constant name). +The `__toString()` method is defined to return the constant name. ```php -(string)Animal::CAT(); // "CAT" +(string)City::SYDNEY(); // "SYDNEY" ``` @@ -171,11 +154,9 @@ The `__toString()` method is defined to return the instance identifier (constant ### map() -Returns an array which maps the constants, and any values. This method is implemented in a sub-class. - -### flip() - -If a map exists, this returns an array with a flipped mapping - value => constant +Returns an array which maps the constant keys to a value. +This method can be optionally implemented in a sub-class. +The default implementation returns an array of keys mapped to `null`. ### keys() @@ -186,42 +167,51 @@ Returns an array of constant keys. Returns an array of values defined in `map()`. If `map()` is not implemented then an array of null values will be returned. -### identifiers() +### names() -Returns an array of all the constant identifiers declared with `const MY_CONST = 'key'` +Returns an array of all the constant names declared with the class. -### constantMap() +### namesAndKeys() -Returns an array of CONST => key, for all of the constant identifiers declared with `const MY_CONST = 'key'`. +Returns an associative array of CONSTANT_NAME => key, for all of the constant names declared within the class. -### getKeyForIdentifier(string $identifier) +### keyForName(string $name) -Returns the key for the given constant identifier. +Returns the key for the given constant name. -### identifierExists(string $identifier) +### nameForKey(string $key) -Returns true if the given identifier is declared as a `const` within the class. +Returns the constant name for the given key (the inverse of `keyForName`). -### valueFor(string $key) +### valueForKey(string $key) Returns the value (or null if not mapped) for the given key (as declared in the `map()` method). -### fromValue(string $value) +### keyForValue(mixed $value) + +Returns the key for the given value (as declared in the `map()` method). + +> Note: If duplicate values are contained within the `map()` method then only the first key will be returned. -Returns the constant for the given value (as declared in the `map()` method). -> Caveats: Only works with single dimension arrays and it will only return the last key if multiple keys have the same value. +### instanceFromName($name) + +Create instance of this Enum from the given constant name. ### instanceFromKey($key) Create instance of this Enum from the given key. -### exists(string $key) +### isValidKey(string $key) Returns true if the given key exists. -### checkExists(string $key) +### isValidName(string $name) + +Returns true if the given constant name (case-sensitive) has been declared in the class. + +### requireValidKey(string $key) -Throws a `InvalidArgumentException` if the given key does NOT exist. +Throws a `\Rexlabs\Enum\Exceptions\InvalidKeyException` if the given key does NOT exist. ## Tests diff --git a/src/Enum.php b/src/Enum.php index cf4109d..0fba383 100644 --- a/src/Enum.php +++ b/src/Enum.php @@ -2,6 +2,7 @@ namespace Rexlabs\Enum; +use Rexlabs\Enum\Exceptions\DuplicateKeyException; use Rexlabs\Enum\Exceptions\InvalidEnumException; use Rexlabs\Enum\Exceptions\InvalidKeyException; use Rexlabs\Enum\Exceptions\InvalidValueException; @@ -14,14 +15,14 @@ */ abstract class Enum { - /** @var array Cache of CONSTANT identifier => value per class */ - public static $constants = []; + /** @var array Cache of constant name => key per class */ + public static $namesToKeysMap = []; - /** @var array Cache of what map() returns per class */ - public static $cachedMap = []; + /** @var array Cache of key => value per class (santized version of what map() returns) */ + public static $keysToValuesMap = []; /** @var string */ - protected $identifier; + protected $name; /** @var mixed */ protected $key; @@ -32,13 +33,13 @@ abstract class Enum /** * Enum constructor. * - * @param string $identifier + * @param string $name * @param mixed $key * @param mixed $value */ - public function __construct(string $identifier, $key, $value) + public function __construct(string $name, $key, $value) { - $this->identifier = $identifier; + $this->name = $name; $this->key = $key; $this->value = $value; } @@ -64,20 +65,11 @@ public static function cachedMap(): array { // Ensure the map is indexed by key $class = static::class; - if (!isset(static::$cachedMap[$class])) { - $cache = null; - $map = static::map(); - if (!empty($map)) { - $isAssoc = array_keys($map) !== range(0, \count($map) - 1); - $cache = $isAssoc ? $map : array_fill_keys(array_values($map), null); - } else { - // No mapping is defined, use the const keys - $cache = array_fill_keys(array_values(static::constantMap()), null); - } - static::$cachedMap[$class] = $cache; + if (!isset(static::$keysToValuesMap[$class])) { + static::$keysToValuesMap[$class] = static::map(); } - return static::$cachedMap[$class]; + return static::$keysToValuesMap[$class]; } /** @@ -88,17 +80,7 @@ public static function cachedMap(): array */ public static function map(): array { - return []; - } - - /** - * Return flipped map where keys become values and vice versa - * - * @return array - */ - public static function flip(): array - { - return array_flip(static::map()) ?? []; + return array_fill_keys(array_values(static::namesAndKeys()), null); } /** @@ -112,28 +94,29 @@ public static function values(): array } /** - * Return the constant identifiers + * Return the constant names + * Each constant declared in the class will be returned. * * @return array */ - public static function identifiers(): array + public static function names(): array { - return array_keys(static::constantMap()); + return array_keys(static::namesAndKeys()); } /** - * Return a map of constant identifiers and their assigned key value. + * Return a map of constant names and their associated key. * * @return array */ - public static function constantMap(): array + public static function namesAndKeys(): array { $class = static::class; - if (!array_key_exists($class, static::$constants)) { - static::$constants[$class] = (new \ReflectionClass($class))->getConstants(); + if (!array_key_exists($class, static::$namesToKeysMap)) { + static::$namesToKeysMap[$class] = (new \ReflectionClass($class))->getConstants(); } - return static::$constants[$class]; + return static::$namesToKeysMap[$class]; } /** @@ -148,58 +131,118 @@ public static function constantMap(): array */ public static function __callStatic($name, $arguments) { - $key = static::getKeyForIdentifier($name); + $key = static::keyForName($name); if ($key === null) { - throw new InvalidEnumException("Invalid constant identifier '{$name}' in " . static::class); + throw new InvalidEnumException("Invalid constant name '{$name}' in " . static::class); } - return new static($name, $key, static::valueFor($key)); + return new static($name, $key, static::valueForKey($key)); } /** - * Get the key for the given constant identifier + * Get the key for the given constant name. * - * @param string $identifier + * @param string $name * * @return null|mixed|string + * @throws InvalidEnumException */ - public static function getKeyForIdentifier(string $identifier) + public static function keyForName(string $name) { - $key = null; - $map = static::constantMap(); + $key = static::namesAndKeys()[$name] ?? null; + if (!$key) { + throw new InvalidEnumException("Invalid constant name: $name in " . static::class); + } - return $map[$identifier] ?? null; + return $key; } /** - * Get the value for a given key + * Get the value for a given key. * - * @param mixed|string $key + * @param mixed|int|string $key * - * @return mixed + * @return mixed|null * @throws InvalidKeyException */ - public static function valueFor($key) + public static function valueForKey($key) { - static::checkExists($key); + static::requireValidKey($key); return static::cachedMap()[$key]; } /** - * Returns the constant for a given value + * Get the value for a given constant name. + * + * @param string $name + * + * @return mixed|null + * @throws InvalidEnumException + */ + public static function valueForName($name) + { + $key = static::keyForName($name); + + return static::cachedMap()[$key] ?? null; + } + + /** + * Get the constant name for a given key. + * + * @param mixed|int|string $key + * + * @return string + * @throws InvalidKeyException + */ + public static function nameForKey($key): string + { + $matches = array_keys(static::namesAndKeys(), $key, true); + $numMatches = \count($matches); + if (!$numMatches) { + throw new InvalidKeyException("Invalid key: $key in " . static::class); + } + if ($numMatches > 1) { + throw new DuplicateKeyException("Unable to resolve name for $key, duplicate matches in " . static::class); + } + return $matches[0]; + } + + /** + * Returns the key for a given value (an inverted search). + * Since keys can be assigned the same value, only the first match will be + * returned. * - * @param str|int $value - * @return Mixed + * @param mixed $value + * @return mixed */ - public static function fromValue($value) + public static function keyForValue($value) { - $flipped = static::flip(); - if ( ! array_key_exists( $value, $flipped )) { + $key = array_search($value, static::cachedMap(), true); + if ($key === false) { throw new InvalidValueException("Value '{$value}' not found in map for " . static::class); } - return $flipped[$value]; + return $key; + } + + /** + * Create instance of this Enum from the constant name. + * This method is case-sensitive, meaning if you declare your constant + * as const MY_CONST = '...', then you will need to provide 'MY_CONST' as + * the argument. + * + * @param string $name + * + * @return static + * @throws InvalidEnumException + */ + public static function instanceFromName($name): self + { + if (!array_key_exists($name, static::namesAndKeys())) { + throw new InvalidEnumException(sprintf('Invalid constant name: %s in %s', $name, static::class)); + } + return static::{$name}(); } /** @@ -212,13 +255,11 @@ public static function fromValue($value) */ public static function instanceFromKey($key): self { - foreach (self::constantMap() as $identifier => $validKey) { - if ($key === $validKey) { - return static::{$identifier}(); - } + $name = array_search($key, static::namesAndKeys(), true); + if ($name === false) { + throw new InvalidKeyException(sprintf('Invalid key: %s in %s', $key, static::class)); } - - throw new InvalidKeyException(sprintf('Invalid key: %s in %s', $key, static::class)); + return static::{$name}(); } /** @@ -228,7 +269,7 @@ public static function instanceFromKey($key): self * * @return boolean */ - public static function exists($key): bool + public static function isValidKey($key): bool { return array_key_exists($key, static::cachedMap()); } @@ -236,54 +277,55 @@ public static function exists($key): bool /** * Check if the key exists or throw an exception * - * @param mixed|string $key + * @param mixed|string|int $key * * @throws InvalidKeyException */ - public static function checkExists($key) + public static function requireValidKey($key) { - if (!static::exists($key)) { + if (!static::isValidKey($key)) { throw new InvalidKeyException("Invalid key: $key in " . static::class); } } /** - * @param string $identifier + * Check if the given constant name is valid. + * @param string $name * * @return bool */ - public static function identifierExists(string $identifier): bool + public static function isValidName(string $name): bool { - return static::getKeyForIdentifier($identifier) !== null; + return array_key_exists($name, static::namesAndKeys()); } /** - * Returns the identifier (constant). - * Same as $enum->identifier(). + * Overloads the string behavior, to return the constant name. + * Same as $instance->name(). * * @return string */ public function __toString() { - return $this->identifier(); + return $this->name(); } /** - * Returns the identifier (constant). + * Returns the constant name. * * @return string */ - public function identifier(): string + public function name(): string { - return $this->identifier; + return $this->name; } /** * Returns the value assigned to the constant declaration. * - * @return mixed|string + * @return mixed|string|int */ - public function key(): string + public function key() { return $this->key; } @@ -301,7 +343,7 @@ public function value() /** * Returns true if this instance is equal to the given key or Enum instance. * - * @param Enum|string $compare + * @param static|mixed $compare * * @return bool * @throws InvalidEnumException @@ -309,13 +351,13 @@ public function value() public function is($compare): bool { if ($compare instanceof self) { - return $compare->identifier() === $this->identifier(); + return $compare->name() === $this->name(); } - if (\is_string($compare)) { + if (\is_scalar($compare)) { return $compare === $this->key(); } - throw new InvalidEnumException('Enum or string expected but ' . \gettype($compare) . ' given.'); + throw new InvalidEnumException('Enum instance or key (scalar) expected but ' . \gettype($compare) . ' given.'); } } diff --git a/src/Exceptions/DuplicateKeyException.php b/src/Exceptions/DuplicateKeyException.php new file mode 100644 index 0000000..420ae68 --- /dev/null +++ b/src/Exceptions/DuplicateKeyException.php @@ -0,0 +1,8 @@ + 'Apple', + static::BANANA => 'Banana', + static::CHERRY => 'Cherry', + static::EGGPLANT => 'Eggplant', + static::AUBERGINE => 'Eggplant', + ]; + } } \ No newline at end of file diff --git a/tests/Stub/Number.php b/tests/Stub/Number.php new file mode 100644 index 0000000..01ee79f --- /dev/null +++ b/tests/Stub/Number.php @@ -0,0 +1,13 @@ +assertEquals([ 'APPLE', 'BANANA', 'CHERRY', - ], Fruit::identifiers()); + 'EGGPLANT', + 'AUBERGINE', + ], Fruit::names()); $this->assertEquals([ 'CAT', 'DOG', 'HORSE', 'PIGEON', - ], Animal::identifiers()); + ], Animal::names()); } public function test_can_get_keys() @@ -35,6 +39,8 @@ public function test_can_get_keys() Fruit::APPLE, Fruit::BANANA, Fruit::CHERRY, + Fruit::EGGPLANT, + Fruit::AUBERGINE, ], Fruit::keys()); $this->assertEquals([ @@ -45,21 +51,23 @@ public function test_can_get_keys() ], Animal::keys()); } - public function test_can_test_identifier_exists() + public function test_can_test_name_exists() { - $this->assertTrue(Fruit::identifierExists('APPLE')); - $this->assertFalse(Fruit::identifierExists('_does_not_exist_')); + $this->assertTrue(Fruit::isValidName('APPLE')); + $this->assertFalse(Fruit::isValidName('_does_not_exist_')); } public function test_get_values() { - // When map() is not defined, should return an array of null + // Number does not provide a map() method and therefore all keys are + // by default mapped to a value of null $this->assertEquals([ null, null, null, - ], Fruit::values()); + null, + ], Number::values()); // When map() is defined, should return all of the mapped values $this->assertEquals([ @@ -73,18 +81,47 @@ public function test_get_values() public function test_can_get_value_for_key() { // Not mapped - $this->assertEquals(null, Fruit::valueFor(Fruit::APPLE)); + $this->assertEquals(null, Number::valueForKey(Number::TWENTY_FOUR)); // Mapped - $this->assertEquals('Kitty-cat', Animal::valueFor(Animal::CAT)); - $this->assertEquals(null, Animal::valueFor(Animal::DOG)); - $this->assertEquals(['you', 'filthy', 'animal'], Animal::valueFor('skyrat')); + $this->assertEquals('Kitty-cat', Animal::valueForKey(Animal::CAT)); + $this->assertEquals(null, Animal::valueForKey(Animal::DOG)); + $this->assertEquals(['you', 'filthy', 'animal'], Animal::valueForKey('skyrat')); } public function test_value_for_invalid_key_throws_exception() { $this->expectException(InvalidKeyException::class); - Fruit::valueFor('_does_not_exist_'); + Fruit::valueForKey('_does_not_exist_'); + } + + public function test_can_get_value_for_name() + { + $this->assertEquals('Apple', Fruit::valueForName('APPLE')); + $this->assertEquals(null, Number::valueForName('TWENTY_FOUR')); + } + + public function test_value_for_invalid_name_throws_exception() + { + $this->expectException(InvalidEnumException::class); + Fruit::valueForName('_does_not_exist_'); + } + + public function test_can_get_name_for_key() + { + $this->assertEquals(Fruit::APPLE()->name(), Fruit::nameForKey(Fruit::APPLE)); + } + + public function test_get_name_for_invalid_key_throws_exception() + { + $this->expectException(InvalidKeyException::class); + Fruit::nameForKey('_does_not_exist_'); + } + + public function test_get_name_for_duplicate_key_throws_exception() + { + $this->expectException(DuplicateKeyException::class); + DuplicateKey::nameForKey(DuplicateKey::FIRST); } public function test_can_instantiate_instance() @@ -96,10 +133,10 @@ public function test_can_instantiate_instance() $this->assertInstanceOf(Animal::class, $animal); } - public function test_can_get_identifier_from_instance() + public function test_can_get_name_from_instance() { - $this->assertEquals('APPLE', Fruit::APPLE()->identifier()); - $this->assertEquals('DOG', Animal::DOG()->identifier()); + $this->assertEquals('APPLE', Fruit::APPLE()->name()); + $this->assertEquals('DOG', Animal::DOG()->name()); } public function test_can_get_key_from_instance() @@ -108,11 +145,16 @@ public function test_can_get_key_from_instance() $this->assertEquals('dog', Animal::DOG()->key()); } - public function test_can_get_value_from_instance() + public function test_can_get_key_from_instance_with_int_keys() { - $this->assertEquals(null, Fruit::APPLE()->value()); - $this->assertEquals(null, Fruit::BANANA()->value()); + $this->assertEquals(10, Number::TEN()->key()); + $this->assertEquals(24, Number::TWENTY_FOUR()->key()); + } + public function test_can_get_value_from_instance() + { + $this->assertEquals('Apple', Fruit::APPLE()->value()); + $this->assertEquals(null, Number::TWENTY_FOUR()->value()); $this->assertEquals('Kitty-cat', Animal::CAT()->value()); $this->assertEquals(null, Animal::HORSE()->value()); $this->assertEquals(['you', 'filthy', 'animal'], Animal::PIGEON()->value()); @@ -137,6 +179,9 @@ public function test_can_compare_enums() $this->assertTrue(Fruit::APPLE()->is(Fruit::APPLE)); $this->assertFalse(Fruit::APPLE()->is(Fruit::BANANA)); $this->assertFalse(Fruit::APPLE()->is('_not_defined_')); + + // When key is not a string + $this->assertTrue(Number::TWENTY_FOUR()->is(24)); } public function test_comparing_enum_to_an_invalid_argument_throws_exception() @@ -145,38 +190,45 @@ public function test_comparing_enum_to_an_invalid_argument_throws_exception() $this->assertTrue(Fruit::APPLE()->is([])); } - public function test_that_getting_an_instance_from_an_invalid_identifier_throws_exception() + public function test_that_getting_an_instance_from_an_invalid_name_throws_exception() { $this->expectException(InvalidEnumException::class); Fruit::NON_EXISTENT(); } - public function test_casting_enum_to_string_returns_identifier() + public function test_casting_enum_to_string_returns_name() { - $this->assertEquals(Fruit::APPLE()->identifier(), (string)Fruit::APPLE()); + $this->assertEquals(Fruit::APPLE()->name(), (string)Fruit::APPLE()); } - public function test_flipable_trait_flips_map() + public function test_get_key_by_value() { - $this->assertEquals([ - 'Corona' => Bevs::BREW, - 'Red Wine' => Bevs::RED_WINE, - 'White Wine' => Bevs::WHITE_WINE, - 'Bundaberg' => Bevs::RUM, - 'Jack Daniels' => Bevs::BOURBON, - ], Bevs::flip()); + $this->assertEquals(Beverage::BREW, Beverage::keyForValue('Corona')); + $this->assertEquals(Beverage::RUM, Beverage::keyForValue('Bundaberg')); } - public function test_flipable_trait_gets_constant_by_value() + public function test_get_key_by_invalid_value_throws_exception() { - $this->assertEquals(Bevs::BREW, Bevs::fromValue('Corona')); - $this->assertEquals(Bevs::RUM, Bevs::fromValue('Bundaberg')); + $this->expectException(InvalidValueException::class); + $this->assertEquals(Beverage::BREW, Beverage::keyForValue('Water')); } - public function test_flipable_trait_throws_exception_with_invalid_value() + public function test_get_key_by_duplicate_value_returns_first() { - $this->expectException(InvalidValueException::class); - $this->assertEquals(Bevs::BREW, Bevs::fromValue('Water')); + // Fruit::EGGPLANT and Fruit::AUBERGINE are both mapped to 'Eggplant' + $this->assertEquals(Fruit::EGGPLANT, Fruit::keyForValue('Eggplant')); + } + + public function test_get_instance_via_name() + { + $fruit = Fruit::instanceFromName('BANANA'); + $this->assertInstanceOf(Fruit::class, $fruit); + + $animal = Animal::instanceFromName('CAT'); + $this->assertInstanceOf(Animal::class, $animal); + + $this->expectException(InvalidEnumException::class); + Animal::instanceFromName('cat'); // Case sensitive } public function test_get_instance_via_key()