Enums in PHP
Category: Blog

I’m not sure what lead me to it, but I stumbled across a blog post on how to implement Enums in PHP via userland code written by Jonathan Hohle. I liked the concept he had, but the implementation was a bit unappealing because it used eval() among other more minor issues. You shouldn’t need to generate Enums at runtime, so I took that as a challenge to find a way to do it at compile time, thus making the code much more efficient. Like Jonathan, I also wanted to maintain support for PHP’s type hinting and it should obviously support the semantics one expects from an enum if feasible. I wasn’t sure of the real value to having the Enums iterable as well like he did, but I figured if possible I should keep support for that too.

First off, we need the Enum base class as follows:

abstract class Enum {
	final public function __toString() {
		return get_class($this);
	}
}

From that miniscule amount of code we have the beginnings of Enum support simply by extending that class as follows:

abstract class DNSRecordType extends Enum {}

class A extends DNSRecordType {}
class CNAME extends DNSRecordType {}
class MX extends DNSRecordType {}

function printDnsRecord(DNSRecordType $type, ...) {
       // We can now be sure $type is a DNSRecordType
}

There are of course a few issues, one of the biggest being that (new A) !== (new A) in the above example code. Here we modify the Enum base class to add support for fixing the equality comparison:

abstract class Enum {
	protected static $instances = array();

	final private function __construct() {}

	final public function __toString() {
		return get_class($this);
	}

	final public static function get($name) {
		if(is_subclass_of($name, "Enum")) {
			if(array_key_exists($name, self::$instances)) {
				return self::$instances[$name];
			} else {
				return self::$instances[$name] = new $name();
			}
		} else {
			throw Exception();
		}
	}

	final public static function __callStatic($name, $args) {
		return self::get($name);
	}
}

Now if we want to use an enum, instead of instantiating the CNAME class directly, we use DNSRecordType::get("CNAME"); on versions of PHP older than 5.3 and in PHP 5.3 we can save a few keystrokes and instead use DNSRecordType::CNAME() to get a CNAME. Obviously in the case of older versions of PHP we are adding a few extra keystrokes, but unfortunately that is required to avoid having to define custom methods for each Enum type due to the lack of the __callStatic() magic method before PHP 5.3.

Ok, so what’s next? Well, I guess our next issue would be that Enums, in the case of languages based on C style semantics, by default don’t have the value of a member as the the name of the member, but rather an integer which increments from zero for each member. Furthermore, in C style enums you can set the value too if you like to any integer of your choice and then each member after that which doesn’t have a specified value will be incremented one higher from it consecutively. To support this style behavior, it forces us partly out of being able to do this at compile time (thus a performance hit). Because of the performance hit, I decided you have to enable this mode before using it, otherwise you get the previous behavior we defined above.

The now updated code is as follows:

abstract class Enum {
	protected static $instances = array();
	protected $value = null;

	protected static $pureMode = false;
	protected static $classesWalked = false;
	protected static $typeCounters = array();
	protected static $classIntValues = array();

	final private function __construct() {}

	final public static function enablePureMode() {
		self::$pureMode = true;
	}

	final protected static function walkClasses() {
		if(self::$pureMode && !self::$classesWalked) {
			foreach(get_declared_classes() as $class) {
				if(is_subclass_of($class, "Enum") && $class !== "Enum") {
					$parent = get_parent_class($class);
					if($parent == "Enum") continue;
					if(!array_key_exists($parent, self::$typeCounters)) {
						self::$typeCounters[$parent] = 0;
					}
					$obj = new $class;
					$objval = $obj->value;
					if(!is_null($objval)) {
						if(!is_numeric($objval)) {
							$nextval = ord($objval) + 1;
						} else {
							$nextval = $objval + 1;
						}
						if($nextval <= self::$typeCounters[$parent]) {
							throw new Exception("Specified enum member value is impossible");
						}
						self::$classIntValues[$class] = $objval;
						self::$typeCounters[$parent] = $nextval;
					} else {
						self::$classIntValues[$class] = self::$typeCounters[$parent]++;
					}
				}
			}
			self::$classesWalked = true;
		}
	}

	final public function __toString() {
		if(self::$pureMode) {
			// delay the performance hit until we actually need the value of one of the enums
			self::walkClasses();
			return (string) self::$classIntValues[get_class($this)];
		} else {
			return get_class($this);
		}
	}

	final public static function get($name) {
		if(is_subclass_of($name, "Enum")) {
			if(array_key_exists($name, self::$instances)) {
				return self::$instances[$name];
			} else {
				return self::$instances[$name] = new $name();
			}
		} else {
			throw new Exception();
		}
	}

	final public static function __callStatic($name, $args) {
		return self::get($name);
	}
}

With the above change, we can now have an enums for months with January's value being 1 for example, like the following:

Enum::enablePureMode();
abstract class Month extends Enum {}
class JAN extends Month { var $value = 1; }
class FEB extends Month {}
class MAR extends Month {}
... etc

We can also do things like the following too:

Enum::enablePureMode();
abstract class Whitespace extends Enum {}
class Newline extends Whitespace { var $value = "\n"; }
class Space extends Whitespace { var $value = " "; }
class Tab extends Whitespace { var $value = "\t"; }
... etc

Pretty nifty eh? Please do note, once you enablePureMode it's enabled for all instances of any Enum because you have already incurred the performance loss. Also, due to the nature of how enablePureMode works, you need to make sure you always have all your Enums defined before you try to use one of their values. This shouldn't be an issue in most cases, but just something to keep in mind. Doing quick tests showed the performance hit from having enablePureMode turned on was pretty trivial (less than a millisecond in some cases), but since you might not need the behavior, no point in taking the performance hit if you don't need to.

The last thing left is to make an iterator of all the members of an Enum. This part is really quick and easy and requires us to add the following method to our Enum class:

final public static function iterator($enum_type) {
	return new EnumIterator($enum_type);
}

As well as also define the following additional class:

class EnumIterator implements Iterator {
	protected $classes = array();
	protected $enum_type;

	public function __construct($enum_type) {
		if(!class_exists($enum_type) || !is_subclass_of($enum_type, "Enum")) throw new Exception("Specified Enum type doesn't exist or is not an Enum!");
		$this->enum_type = $enum_type;
		foreach(get_declared_classes() as $class) {
			if(is_subclass_of($class, $this->enum_type)) {
				$this->classes[] = $class;
			}
		}
	}

	public function current() {
		return current($this->classes);
	}

	public function key() {
		return key($this->classes);
	}

	public function next() {
		next($this->classes);
		return $this->current();
	}

	public function rewind() {
		return reset($this->classes);
	}

	public function valid() {
		return (bool) $this->current();
	}
}

An example of using the iterator is as follows:

echo "DNSRecordType:\n";
foreach(Enum::iterator("DNSRecordType") as $type) {
	echo "\t" . $type . " has value " . (Enum::get($type)) . "\n";
}

To get a copy of the code including several example usage demonstrations, please visit the following URL: http://github.com/jsjohnst/php_class_lib/tree/master/classes/types/enum

If you find this useful, please let me know!

-Jeremy

Tags: , , , ,

6 Responses to “Enums in PHP”

  1. [...] this new post Jeremy Johnstone looks at creating a class to add that’s missing from the basic datatype set [...]

  2. Mirin blog says:

    Enum v PHP…

    Jedna z věcí, která v PHP chybí je podpora výčtového typu – enum. Je to vlastně množina konstant – enumerátorů, každá má svůj identifikátor. Proměnná takového výčtového typu pak nabývá jednu z konstant. Bez výčtového typu se …

  3. zanshine says:

    Here is a github library for handling type-safe enumerations in php:

    This library handle classes generation, classes caching and it implements the Type Safe Enumeration design pattern, with several helper methods for dealing with enums, like retrieving an ordinal for enums sorting, or retrieving a binary value, for enums combinations.

    The generated code use a plain old php template file, which is also configurable, so you can provide your own template.

    It is full test covered with phpunit.

    php-enums on github (feel free to fork)

    Usage: (@see usage.php, or unit tests for more details)
    —————————————-

    getName()\n";
    foreach (FruitsEnum::iterator() as $enum)
    {
    echo " " . $enum->getName() . "\n";
    }

    echo "->getValue()\n";
    foreach (FruitsEnum::iterator() as $enum)
    {
    echo " " . $enum->getValue() . "\n";
    }

    echo "->getOrdinal()\n";
    foreach (CachedFruitsEnum::iterator() as $enum)
    {
    echo " " . $enum->getOrdinal() . "\n";
    }

    echo "->getBinary()\n";
    foreach (CachedFruitsEnum::iterator() as $enum)
    {
    echo " " . $enum->getBinary() . "\n";
    }

    Output:
    -------

    FruitsEnum::APPLE() == FruitsEnum::APPLE(): bool(true)
    FruitsEnum::APPLE() == FruitsEnum::ORANGE(): bool(false)
    FruitsEnum::APPLE() instanceof Enum: bool(true)
    FruitsEnum::APPLE() instanceof FruitsEnum: bool(true)
    ->getName()
    APPLE
    ORANGE
    RASBERRY
    BANNANA
    ->getValue()
    apple
    orange
    rasberry
    bannana
    ->getValue() when values have been specified
    pig
    dog
    cat
    bird
    ->getOrdinal()
    1
    2
    3
    4
    ->getBinary()
    1
    2
    4
    8

  4. zanshine says:

    In the last comment, a part of the usage code have been eaten …

    So, here it is, without the nasty php open tag ;0) :

    Usage: (@see usage.php, or unit tests for more details)
    —————————————-

    //require the library
    require_once __DIR__ . ‘/src/Enum.func.php’;

    //if you don’t have a cache directory, create one
    @mkdir(__DIR__ . ‘/cache’);
    EnumGenerator::setDefaultCachedClassesDir(__DIR__ . ‘/cache’);

    //Class definition is evaluated on the fly:
    Enum(‘FruitsEnum’, array(‘apple’ , ‘orange’ , ‘rasberry’ , ‘bannana’));

    //Class definition is cached in the cache directory for later usage:
    Enum(‘CachedFruitsEnum’, array(‘apple’ , ‘orange’ , ‘rasberry’ , ‘bannana’), ‘\my\company\name\space’, true);

    echo ‘FruitsEnum::APPLE() == FruitsEnum::APPLE(): ‘;
    var_dump(FruitsEnum::APPLE() == FruitsEnum::APPLE()) . “\n”;

    echo ‘FruitsEnum::APPLE() == FruitsEnum::ORANGE(): ‘;
    var_dump(FruitsEnum::APPLE() == FruitsEnum::ORANGE()) . “\n”;

    echo ‘FruitsEnum::APPLE() instanceof Enum: ‘;
    var_dump(FruitsEnum::APPLE() instanceof Enum) . “\n”;

    echo ‘FruitsEnum::APPLE() instanceof FruitsEnum: ‘;
    var_dump(FruitsEnum::APPLE() instanceof FruitsEnum) . “\n”;

    echo “->getName()\n”;
    foreach (FruitsEnum::iterator() as $enum)
    {
    echo ” ” . $enum->getName() . “\n”;
    }

    echo “->getValue()\n”;
    foreach (FruitsEnum::iterator() as $enum)
    {
    echo ” ” . $enum->getValue() . “\n”;
    }

    echo “->getOrdinal()\n”;
    foreach (CachedFruitsEnum::iterator() as $enum)
    {
    echo ” ” . $enum->getOrdinal() . “\n”;
    }

    echo “->getBinary()\n”;
    foreach (CachedFruitsEnum::iterator() as $enum)
    {
    echo ” ” . $enum->getBinary() . “\n”;
    }

    Output:
    ——-

    FruitsEnum::APPLE() == FruitsEnum::APPLE(): bool(true)
    FruitsEnum::APPLE() == FruitsEnum::ORANGE(): bool(false)
    FruitsEnum::APPLE() instanceof Enum: bool(true)
    FruitsEnum::APPLE() instanceof FruitsEnum: bool(true)
    ->getName()
    APPLE
    ORANGE
    RASBERRY
    BANNANA
    ->getValue()
    apple
    orange
    rasberry
    bannana
    ->getValue() when values have been specified
    pig
    dog
    cat
    bird
    ->getOrdinal()
    1
    2
    3
    4
    ->getBinary()
    1
    2
    4
    8

  5. Jordan says:

    Great example! Since php is loosely typed you can gain from using enums for comparisons. For example extending this to add a check if a valid value. I added a few features and put it up here – http://www.focusandblur.com/2011/08/enum-php/

Leave a Comment

Spam Protection by WP-SpamFree