Skip to content

Instantly share code, notes, and snippets.

@mrclay
Last active August 29, 2015 14:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mrclay/33aa40214256c8aac52a to your computer and use it in GitHub Desktop.
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.
<?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];
}
}
<?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 {}
<?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'));
@mrclay
Copy link
Author

mrclay commented Aug 25, 2014

Next step is adding a way to add initialize other properties. See the planets example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment