diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -168,6 +168,8 @@ 'PhutilEmailAddress' => 'parser/PhutilEmailAddress.php', 'PhutilEmailAddressTestCase' => 'parser/__tests__/PhutilEmailAddressTestCase.php', 'PhutilEmptyAuthAdapter' => 'auth/PhutilEmptyAuthAdapter.php', + 'PhutilEnum' => 'object/PhutilEnum.php', + 'PhutilEnumTestCase' => 'object/__tests__/PhutilEnumTestCase.php', 'PhutilErrorHandler' => 'error/PhutilErrorHandler.php', 'PhutilErrorHandlerTestCase' => 'error/__tests__/PhutilErrorHandlerTestCase.php', 'PhutilErrorTrap' => 'error/PhutilErrorTrap.php', @@ -200,9 +202,11 @@ 'PhutilIPAddressTestCase' => 'ip/__tests__/PhutilIPAddressTestCase.php', 'PhutilInRequestKeyValueCache' => 'cache/PhutilInRequestKeyValueCache.php', 'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php', + 'PhutilInvalidEnumImplementationException' => 'object/PhutilInvalidEnumImplementationException.php', 'PhutilInvalidRuleParserGeneratorException' => 'parser/generator/exception/PhutilInvalidRuleParserGeneratorException.php', 'PhutilInvalidStateException' => 'exception/PhutilInvalidStateException.php', 'PhutilInvalidStateExceptionTestCase' => 'exception/__tests__/PhutilInvalidStateExceptionTestCase.php', + 'PhutilInvalidTestEnum' => 'object/__tests__/PhutilInvalidTestEnum.php', 'PhutilInvisibleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilInvisibleSyntaxHighlighter.php', 'PhutilIrreducibleRuleParserGeneratorException' => 'parser/generator/exception/PhutilIrreducibleRuleParserGeneratorException.php', 'PhutilJIRAAuthAdapter' => 'auth/PhutilJIRAAuthAdapter.php', @@ -338,6 +342,7 @@ 'PhutilSystem' => 'utils/PhutilSystem.php', 'PhutilSystemTestCase' => 'utils/__tests__/PhutilSystemTestCase.php', 'PhutilTerminalString' => 'xsprintf/PhutilTerminalString.php', + 'PhutilTestEnum' => 'object/__tests__/PhutilTestEnum.php', 'PhutilTestPhobject' => 'object/__tests__/PhutilTestPhobject.php', 'PhutilTortureTestDaemon' => 'daemon/torture/PhutilTortureTestDaemon.php', 'PhutilTranslation' => 'internationalization/PhutilTranslation.php', @@ -690,6 +695,7 @@ 'PhutilEmailAddress' => 'Phobject', 'PhutilEmailAddressTestCase' => 'PhutilTestCase', 'PhutilEmptyAuthAdapter' => 'PhutilAuthAdapter', + 'PhutilEnumTestCase' => 'PhutilTestCase', 'PhutilErrorHandler' => 'Phobject', 'PhutilErrorHandlerTestCase' => 'PhutilTestCase', 'PhutilErrorTrap' => 'Phobject', @@ -722,9 +728,11 @@ 'PhutilIPAddressTestCase' => 'PhutilTestCase', 'PhutilInRequestKeyValueCache' => 'PhutilKeyValueCache', 'PhutilInteractiveEditor' => 'Phobject', + 'PhutilInvalidEnumImplementationException' => 'Exception', 'PhutilInvalidRuleParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilInvalidStateException' => 'Exception', 'PhutilInvalidStateExceptionTestCase' => 'PhutilTestCase', + 'PhutilInvalidTestEnum' => 'PhutilEnum', 'PhutilInvisibleSyntaxHighlighter' => 'Phobject', 'PhutilIrreducibleRuleParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilJIRAAuthAdapter' => 'PhutilOAuth1AuthAdapter', @@ -859,6 +867,7 @@ 'PhutilSystem' => 'Phobject', 'PhutilSystemTestCase' => 'PhutilTestCase', 'PhutilTerminalString' => 'Phobject', + 'PhutilTestEnum' => 'PhutilEnum', 'PhutilTestPhobject' => 'Phobject', 'PhutilTortureTestDaemon' => 'PhutilDaemon', 'PhutilTranslation' => 'Phobject', diff --git a/src/object/PhutilEnum.php b/src/object/PhutilEnum.php new file mode 100644 --- /dev/null +++ b/src/object/PhutilEnum.php @@ -0,0 +1,217 @@ +key = static::search($value); + $this->value = $value; + } + + /** + * Returns a @{class:PhutilEnum} instance from a class constant. + * + * TODO + * + * @param string + * @param list + * @return static + * @task construct + */ + final public static function __callStatic($name, $arguments) { + $array = static::toArray(); + + if (isset($array[$name])) { + return static::initializeEnum($array[$name]); + } + + throw new BadMethodCallException( + pht( + 'No static method or enumeration constant "%s" in %s', + $name, + get_called_class())); + } + + /** + * Returns the enumeration value, as a string. + * + * @return string + */ + final public function __toString() { + return (string)$this->value; + } + + /** + * TODO + * + * @param wild + * @return ??? + * @task construct + */ + final private static function initializeEnum($value) { + if (!array_key_exists($value, static::$cache)) { + static::$cache[$value] = new static($value); + } + + return static::$cache[$value]; + } + + /** + * Returns the enumeration key. + * + * @return wild + * @task enum + */ + final public function getKey() { + return $this->key; + } + + /** + * Returns the enumeration value. + * + * @return wild + * @task enum + */ + final public function getValue() { + return $this->value; + } + + /** + * Returns the names (keys) of all constants in the enumerator class. + * + * @return array + * @task enum + */ + final public static function keys() { + return array_keys(static::toArray()); + } + + /** + * Returns instances of the @{class:PhutilEnum} class for all enumeration + * constants. + * + * @return map Constant name in key, @{class:PhutilEnum} + * instance in value. + * @task enum + */ + final public static function values() { + $values = array(); + + foreach (static::toArray() as $key => $value) { + $values[$key] = static::initializeEnum($value); + } + + return $values; + } + + /** + * Returns all possible enumeration values as an array. + * + * @return map Constant name in key, constant value in value. + * @task enum + */ + final public static function toArray() { + $class = get_called_class(); + return id(new ReflectionClass($class))->getConstants(); + } + + /** + * Return the enumeration key for a specified value. + * + * NOTE: This method assumes that enumeration values are unique, see + * @{method:assertValidImplementation}. + * + * @param wild The specification value to search for. + * @return wild The enumeration key, or `null` is the specified value does + * not exist. + * @task enum + */ + final private static function search($value) { + $key = array_search($value, static::toArray(), true); + + if ($key === false) { + $key = null; + } + + return $key; + } + + /** + * Asserts that a @{class:PhutilEnum} implementation is valid. + * + * Specifically, this method asserts that enumeration values are unique. For + * example, the following @{class:PhutilEnum} implementation is invalid: + * + * ```lang=php, counterexample + * final class PhutilInvalidEnum extends PhutilEnum { + * const SOMETHING = 'value'; + * const SOMETHING_ELSE = 'value'; + * } + * ``` + * + * @return void + * @task assert + */ + final private static function assertValidImplementation() { + $array = static::toArray(); + + if (array_values($array) != array_unique(array_values($array))) { + throw new PhutilInvalidEnumImplementationException( + pht( + '%s is not a valid %s implementation. Valid %s implementation '. + 'must have unique enumeration value.', + get_called_class(), + __CLASS__, + __CLASS__)); + } + } + +} diff --git a/src/object/PhutilInvalidEnumImplementationException.php b/src/object/PhutilInvalidEnumImplementationException.php new file mode 100644 --- /dev/null +++ b/src/object/PhutilInvalidEnumImplementationException.php @@ -0,0 +1,3 @@ +assertEqual('one', (string)$enum); + + $enum = PhutilTestEnum::TWO(); + $this->assertEqual('two', (string)$enum); + + $caught = null; + try { + PhutilTestEnum::THREE(); + } catch (BadMethodCallException $ex) { + $caught = $ex; + } + $this->assertTrue($caught instanceof BadMethodCallException); + } + + public function testSingleton() { + $this->assertEqual( + PhutilTestEnum::ONE(), + PhutilTestEnum::ONE()); + } + + public function testGetKey() { + $this->assertEqual('ONE', PhutilTestEnum::ONE()->getKey()); + } + + public function testGetValue() { + $this->assertEqual('one', PhutilTestEnum::ONE()->getValue()); + } + + public function testKeys() { + $keys = array('ONE', 'TWO'); + $this->assertEqual($keys, PhutilTestEnum::keys()); + } + + public function testValues() { + $values = array( + 'ONE' => PhutilTestEnum::ONE(), + 'TWO' => PhutilTestEnum::TWO(), + ); + $this->assertEqual($values, PhutilTestEnum::values()); + } + + public function testToArray() { + $array = array('ONE' => 'one', 'TWO' => 'two'); + $this->assertEqual($array, PhutilTestEnum::toArray()); + } + + public function testInvalidImplementation() { + $caught = null; + try { + PhutilInvalidTestEnum::ONE(); + } catch (Exception $ex) { + $caught = $ex; + } + + $this->assertTrue( + $caught instanceof PhutilInvalidEnumImplementationException); + } + +} diff --git a/src/object/__tests__/PhutilInvalidTestEnum.php b/src/object/__tests__/PhutilInvalidTestEnum.php new file mode 100644 --- /dev/null +++ b/src/object/__tests__/PhutilInvalidTestEnum.php @@ -0,0 +1,6 @@ +