Java-inspired Enumerated Types in PHP

The Problem

In an application we've been developing in PHP recently here at RedBottle, I found myself with a need to represent the "account state" of a hosting client, which can be either active", "disabled", "expired", etc. Having written applications with similar concerns in the past, I felt that the perfect tool for this job would normally be an enumerated type, or enum for short.

If you're experienced with writing in a language like C, Java (after 1.5), or a .NET language (C#, VB.net, etc), you're undoubtedly familiar with enums and how useful they are for representing a fixed set of values. For example, you could declare an enum to represent the days of the week or the months of the year; you could even declare one for the types of protocol messages your application is expecting to handle.

Unfortunately, PHP is much maligned for not having native support for enums. Most PHP developers who want to represent a fixed range of values have to define a constant for each value that is expected, declare that their functions take a standard scalar value, and just hope that callers will not provide a value that isn't outside the defined and expected range. Developers are intelligent people, but having to trust that someone won't provide a value you're not expecting can lead to unexpected behavior down the road when someone accidentally or maliciously does.

The Existing Solutions

There have been many object-oriented solutions proposed to address this limitation. I even looked back through some code I wrote a few years ago to solve this problem, but discovered that my previous implementation was difficult to maintain and counter-intuitive to use. In my searches across the internet, I found similar drawbacks in all of the implementations I encountered.

Here's a review of the implementations that are out there, and their drawbacks:

  • Enums in PHP

    (site might be down; see the archived version)

    Benjamin Eberlei provides an implementation that relies only on constants, inheritance, reflection, and __toString(). Unfortunately, it's not very elegant – constructing a single enum element requires passing one of the constants to the enum constructor (i.e. $monday = new Day(Day::Monday), which seems redundant. There's also nothing to guarantee that a sub-class can't circumvent the enum range just by extending the class and defining more constants. In addition, by relying on __toString(), Eberlei's implementation may demonstrate unexpected behavior if an implementor chooses to provide his or her own __toString() override in an enum. Lastly, there's no good way to get elements by index, or iterate over the elements of the enum in any way, which is something most other languages support.

  • Enums in PHP - A Native Implementation

    Jonathan Hohle provides an implementation that is flexible and rather straightforward to use. Unfortunately, he relies heavily on eval'd code, which is considered a poor practice for the same reasons that dynamic SQL is – all of the the code is written in hard-to-maintain strings and you can't guarantee that the generated code will work 100% of the time.

  • Enums in PHP | Ramblings of a Geek

    Jeremy Johnstone provides an implementation that doesn't use eval'd code, but since it relies on each element being a new sub-class, it requires developers to write a lot of code to define each element of the enumeration. In addition, as with Everlei's implementation, since any developer could just write classes that extend your enum, there's nothing to guarantee that the range of values is fixed and constant. Lastly, Johnstone's implementation provides an awkward feature called "pure mode" that implementers must enable if they want all elements of the enum to have sequentially-numbered ordinal values – a feature you can readily take for granted without special handling in C or Java enums.

A Better Solution Inspired by Java

Java's implementation of enums is by far one of the most type-safe because, unlike in C or .NET, you can't just cast an integer as a enum value, or vice-versa. In addition, in the Java implementation, all enum elements are instances of a custom class that you can add methods to. When you combine those features with Java's built-in support for enum element sets, which replaces many of the binary AND/OR tricks you had to do in C and C++, you can clearly see how the Java implementation is superior.

Emulating Java's enum implementation, while still taking into account PHP's many idiosyncrasies, we came up with an implementation that we are quite satisfied with.

Declaration

In our implementation, you declare a new enumerated type as follows:

  final class Day extends Enum {
    public static function enum() {
      return self::declareElements(
      __CLASS__,
      array(
        'MONDAY',
        'TUESDAY',
        'WEDNESDAY',
        'THURSDAY',
        'FRIDAY',
        'SATURDAY',
        'SUNDAY'
      ));
    }
  }

This declares a new enumerated type called "Day", with an enumerated element for each day of the week.

The __CLASS__ value is required to be passed along as the first parameter. This is an unfortunate side-effect of PHP's inability to dynamically determine the class name from within a static method; if you think that get_class() or self would serve this purpose, you're wrong. Using them within a static method will always return the class that the method was originally declared in, not the sub-class that the static method is being called on.

Additionally, as much as I wish it was avoidable (since it's so easy to forget), it's good practice to declare the enumerated type as final (see above) in order to prevent other developers from writing sub-classes that expose additional enum elements/values outside the original range of values declared in the initial enumerated type. As you may recall from my review of the other implementations, this addresses the same issue with our implementation that I pointed out in both Eberlei's and Johnstone's implementations. Sadly, it's not something that our implementation can just give you "for free".

Accessing Enum Elements

After declaring the enumerated type as above, the base Enum class takes care of the rest; it instantiates a new instance of the custom enum type for each element you've specified. You can then get access to the values of the enum, as follows:

$days = Day::enum();

print "My favorite day is " . $days->FRIDAY . ".\n";

Which prints:

My favorite day is FRIDAY.

Iterating Over Enums and Accessing Element Values

By default, the implementation assigns each enum element an ordinal value, with the first element being assigned zero. The value of each element is accessible via the built-in getValue() method.

This means that the following code:

foreach (Day::enum() as $day) {
  print $day->getName() . ' = ' . $day->getValue() . "\n";
}

...would print:

MONDAY = 0
TUESDAY = 1
WEDNESDAY = 2
THURSDAY = 3
FRIDAY = 4
SATURDAY = 5
SUNDAY = 6

Assigning Element Values

In place of the ordinal values assigned by default, enum elements can be assigned custom values, instead, as in the following example:

  final class Day2 extends Enum {
    public static function enum() {
      return self::declareElements(
        __CLASS__,
        array
        (
          'MONDAY',
          'TUESDAY'     => 13,
          'WEDNESDAY',
          'THURSDAY'    => 22,
          'FRIDAY',
          'SATURDAY'    => 'Value',
          'SUNDAY'
      ));
    }
  }

  foreach (Day2::enum() as $day) {
    print $day->getName() . ' = ' . $day->getValue() . "\n";
  }

This would result in the following output:

MONDAY = 0
TUESDAY = 13
WEDNESDAY = 14
THURSDAY = 22
FRIDAY = 23
SATURDAY = Value
SUNDAY = 24

Limiting Parameter Ranges at Run-time with Type Hinting

Perhaps the most important use case for enums is to restrict the range of values that can be passed to function and method calls. Although PHP is not statically typed, it supports "type hinting" which allows you to specify that you're expecting a parameter to be an instance of a specific type. If specified, PHP will then perform checks at run-time to guarantee that parameters are of the expected type. Therefore, we can combine our enum implementation with type checking to restrict parameter value ranges.

Here's an example of a function using type hinting in this way:

function scheduleEvent($event, Day $day) {
  print "The " . $event . " has been scheduled on " . $day->getName() . ".\n";
}

Here is an example of how you would call the above function:

scheduleEvent('hiking trip', $days->SATURDAY);

However, if you tried calling it with a value that is not an enumerated value, like so:

scheduleEvent('hiking trip', 'SATURDAY');

...you would get the error:

Catchable fatal error: Argument 2 passed to scheduleEvent() must be an instance of Day, string given

Likewise, if you tried calling it with a value from a different enumerated type, like so:

final class Month extends Enum {
  public static function enum() {
    return self::declareElements(
      __CLASS__,
      array
      (
        'JANUARY',
        'FEBRUARY',
        'MARCH',
        'APRIL',
        'MAY',
        'JUNE',
        'JULY',
        'AUGUST',
        'SEPTEMBER',
        'OCTOBER',
        'NOVEMBER',
        'DECEMBER'
    ));
  }
}

$months = Month::enum();

scheduleEvent('hiking trip', $months->MAY);

...you would get this error:

Catchable fatal error: Argument 2 passed to scheduleEvent() must be an instance of Day, instance of Month given

In this way, PHP's type hinting can do a lot for us at run-time to make sure we only get values we're expecting.

Using Elements in switch Statements

Of course no implementation of enums would be complete without support for switch statements. Here's an example:

$day = $days->SATURDAY;

switch ($day) {
  case $days->MONDAY:
    $value = "First day of the work week";
    break;

  case $days->TUESDAY:
    $value = "Better than Monday";
    break;

  case $days->WEDNESDAY:
    $value = "Work week is half over";
    break;

  case $days->THURSDAY:
    $value = "It's almost Friday";
    break;

  case $days->FRIDAY:
    $value = "Awesome. It's Friday!";
    break;

  case $days->SATURDAY:
    $value = "Time to sleep in";
    break;

  case $days->SUNDAY:
    $value = "No! It's the last day of the weekend...";
    break;
}

print $value . "\n";

This will print:

Time to sleep in

Adding Custom Methods

As in the Java implementation of enums, in our implementation each enumerated type is a class, so you can add your own custom methods.

Here's an example:

final class Day3 extends Enum {
  public static function enum() {
    return self::declareElements(
      __CLASS__,
      array
      (
        'MONDAY',
        'TUESDAY',
        'WEDNESDAY',
        'THURSDAY',
        'FRIDAY',
        'SATURDAY',
        'SUNDAY'
    ));
  }

  public function printMe() {
    print "My day is " . $this->name . "\n";
  }
}

foreach (Day3::enum() as $day) {
  /* @var $day Day3 */
  $day->printMe();
}

This will print:

My day is MONDAY
My day is TUESDAY
My day is WEDNESDAY
My day is THURSDAY
My day is FRIDAY
My day is SATURDAY
My day is SUNDAY

Element Set Operations with EnumSets

In our implementation, the result of a Enum::declareElements() call is an instance of EnumSet – an immutable collection of all of the elements of the enumerated type. If you follow the pattern for declaring a new enumerated type that I established in prior examples, that means that your enumerated types' enum() method actually returns an EnumSet instance as well.

That means that in almost all of the examples above, we were working with the EnumSet instance rather than Enum elements directly. As expected, the EnumSet implements the Iterator SPL interface, which is why we can iterate over it with foreach loops. It also implements a __get() method, which is what allows us to directly access enumeration elements out of the set as if they were member fields.

In addition to that standard fare, our EnumSet implementation also provides some pretty useful functionality, inspired by Java's EnumSet implementation. This includes a tersely named of() method, which produces an EnumSet instance that contains a sub-set of only the specified enum elements; and a more useful range() method, which produces an EnumSet instance that contains a sub-set with the specified range of elements.

Here's an example of of() in action:

$classDays = $days->of($days->TUESDAY, $days->THURSDAY);

print "I only have classes on:\n";

foreach ($classDays as $day) {
  /* @var $day Day */
  print $day->getName() . "\n";
}

Which prints:

I only have classes on:
TUESDAY
THURSDAY

And, here's an example of range() in action:

print "I should be free on:\n";

foreach ($days->range($days->FRIDAY, $days->SUNDAY) as $day) {
  /* @var $day Day */
  print $day->getName() . "\n";
}

Which prints:

I should be free on:
FRIDAY
SATURDAY
SUNDAY

If you want something a little less verbose, both of() and range() can accept element names instead of instances, as well:

$classDays2 = $days->of('TUESDAY', 'THURSDAY');

print "I only have classes on:\n";

foreach ($classDays2 as $day) {
  /* @var $day Day */
  print $day->getName() . "\n";
}

And:

print "I should be free on:\n";

foreach ($days->range('FRIDAY', 'SUNDAY') as $day) {
  /* @var $day Day */
  print $day->getName() . "\n";
}

More

Both the Enum and EnumSet classes have a little bit more functionality than I've presented here, mostly to do with locating particular elements by name or value. I encourage you to look through the inline PHPDoc documentation.

Where to Get it

Our enum implementation is licensed under version 3 of the Lesser GNU General Public License, which basically means that it can be included in proprietary applications as long as the source code for any modifications (if any) to the enum implementation itself are released.

You can get the implementation off of the RedBottle Design server.