Last active
August 29, 2015 14:05
-
-
Save mrclay/33aa40214256c8aac52a to your computer and use it in GitHub Desktop.
Enum types in PHP. This is the closest thing to the Java model (each value is an instance) I could get with good IDE usability.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Enums; | |
/** | |
* Base class for an Enum type where each value is a class instance. Possible values are | |
* parsed from PHPdoc comments (must be static methods that return the type and are all caps) | |
*/ | |
abstract class Base { | |
/** | |
* @var string | |
*/ | |
protected $name; | |
/** | |
* @var int | |
*/ | |
protected $ordinal; | |
/** | |
* @param string $name | |
* @param int $ordinal | |
*/ | |
final protected function __construct($name, $ordinal) { | |
$this->name = $name; | |
$this->ordinal = $ordinal; | |
} | |
/** | |
* Get the name (uppercase) of the value | |
* | |
* @return string | |
*/ | |
final public function __toString() { | |
return $this->name; | |
} | |
/** | |
* Get the ordinal number of the value in the type | |
* | |
* @return int | |
*/ | |
final public function ordinal() { | |
return $this->ordinal; | |
} | |
/** | |
* Returns a negative integer, zero, or a positive integer as this object is | |
* less than, equal to, or greater than the specified object. | |
* | |
* @param object|string $value Compared value. Must be of same type or a string representation of | |
* one of its values. | |
* @return int | |
* @throws \InvalidArgumentException | |
*/ | |
final public function compareTo($value) { | |
if (is_string($value)) { | |
$value = static::fromString($value); | |
} | |
if (!($value instanceof $this)) { | |
throw new \InvalidArgumentException("compareTo() accepts value of same type or string"); | |
} | |
return $this->ordinal - $value->ordinal(); | |
} | |
/** | |
* Loosely compare the value with another value/string. The values are equivalent if | |
* they have the same string value. | |
* | |
* @param object|string $value Compared value. Must be of same type or a string representation of | |
* one of its values. | |
* @return bool | |
* @throws \InvalidArgumentException | |
*/ | |
final public function equals($value) { | |
if (is_string($value)) { | |
$value = static::fromString($value); | |
} | |
if (!($value instanceof $this)) { | |
throw new \InvalidArgumentException("equals() accepts value of same type or string"); | |
} | |
return $value->name === $this->name; | |
} | |
/** | |
* Get a value instance from a string | |
* | |
* @param string $value | |
* @param mixed $default Value returned if not a valid value | |
* @return object|mixed | |
*/ | |
final static public function fromString($value, $default = null) { | |
$value = strtoupper($value); | |
$values = static::getValues(); | |
return isset($values[$value]) ? $values[$value] : $default; | |
} | |
/** | |
* Get all the instances of a type with their string values as keys | |
* | |
* @return object[] | |
* @throws \InvalidArgumentException | |
*/ | |
static public function getValues() { | |
return Base::getInstances(get_called_class()); | |
} | |
/** | |
* Get a particular instance of an Enum type | |
* | |
* <code> | |
* $maybe = NoMaybeYes::MAYBE(); | |
* </code> | |
* | |
* @param string $name | |
* @param array $args | |
* @return object | |
* @throws \BadMethodCallException | |
*/ | |
final public static function __callStatic($name, $args) { | |
$values = static::getValues(); | |
if (empty($values[$name])) { | |
throw new \BadMethodCallException(); | |
} | |
return $values[$name]; | |
} | |
/** | |
* Get all the instances of a type with their string values as keys | |
* | |
* @param string $class | |
* @return object[] | |
* @throws \InvalidArgumentException | |
*/ | |
final static private function getInstances($class) { | |
static $instances = array(); | |
if (empty($instances[$class])) { | |
// parse from Phpdoc @method declarations | |
$refClass = new \ReflectionClass($class); | |
$name = preg_quote('\\' . $refClass->getName(), '~'); | |
$shortName = $refClass->getShortName(); | |
$doc = $refClass->getDocComment(); | |
preg_match_all("~@method\\s+static\\s+(?:$shortName|$name)\\s+([A-z0-9]+)\\(\\)[^\n]*\n~", $doc, $m); | |
$names = $m[1]; | |
// build all instances | |
foreach ($names as $i => $name) { | |
$instances[$class][$name] = new $class($name, $i); | |
} | |
} | |
return $instances[$class]; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Foo; | |
/** | |
* Example Enum type. | |
* | |
* Values are pulled from phpDoc below: | |
* | |
* @method static NoMaybeYes NO() No means no. | |
* @method static NoMaybeYes MAYBE() This means maybe! | |
* @method static \Foo\NoMaybeYes YES() Sure, buddy. | |
*/ | |
class NoMaybeYes extends \Enums\Base {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
require __DIR__ . '/Enums_Base.php'; | |
require __DIR__ . '/Foo_NoMaybeYes.php'; | |
use Foo\NoMaybeYes; | |
/** | |
* Like Java, each enum type is a collection of instances with unique string names. Whereas Java | |
* presents each one as a class constant, in PHP the closest we can come is for each to be a static | |
* method. | |
* | |
* PhpDoc static method documentation allows us to get IDE/static analysis support while also giving the | |
* enum engine the names of the type. | |
*/ | |
$yes = NoMaybeYes::YES(); | |
$yes2 = NoMaybeYes::YES(); | |
/** | |
* We have a convenience method for getting the value from a string. | |
*/ | |
$yes3 = NoMaybeYes::fromString('yes'); | |
/** | |
* All values with same name and type are identical. When a type is first used, all the instances are | |
* created and only those references are handed out. Public construction is not allowed. | |
*/ | |
assert($yes instanceof NoMaybeYes); | |
assert($yes === $yes2 && $yes === $yes3); | |
/** | |
* Getting the name and ordinal | |
*/ | |
assert((string)$yes === 'YES'); | |
assert($yes->ordinal() === 2); | |
$no = NoMaybeYes::NO(); | |
/** | |
* fromString only returns values for valid strings | |
*/ | |
assert(null === NoMaybeYes::fromString('not in set')); | |
/** | |
* Unlike Java, we allow comparing with strings as long as the string is a valid name | |
*/ | |
assert($yes->equals($yes)); | |
assert($yes->equals('YeS')); | |
assert(!$yes->equals($no)); | |
assert(!$yes->equals('no')); | |
assert($no->compareTo($no) == 0); | |
assert($no->compareTo($yes) < 0); | |
assert($no->compareTo('maybe') < 0); | |
assert($yes->compareTo('maybe') > 0); | |
/** | |
* Invalid names cannot be compared | |
*/ | |
foreach (array('not in set', array(), new stdClass(), 2) as $value) { | |
try { | |
$yes->equals($value); | |
assert(false, 'failed to catch value: ' . var_export($value, true)); | |
} catch (InvalidArgumentException $e) { | |
// pass | |
} | |
try { | |
$yes->compareTo($value); | |
assert(false, 'failed to catch value: ' . var_export($value, true)); | |
} catch (InvalidArgumentException $e) { | |
// pass | |
} | |
} | |
/** | |
* You can get all the values keyed by their name | |
*/ | |
$values = NoMaybeYes::getValues(); | |
assert($values['YES'] === $yes); | |
assert($values['NO'] === $no); | |
assert($values['MAYBE'] instanceof NoMaybeYes); | |
assert(array_keys($values) === array('NO', 'MAYBE', 'YES')); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Next step is adding a way to add initialize other properties. See the planets example.