Whether you're drawing customers into your restaurant for dinner, or friends over to your house for dessert, these tips will whet everyone's appetite.
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.
RBD News
New RedBottle Website
ROCHESTER, NY - Red Bottle Design, LLC has launched its new corporate website running on Drupal 7.
Latest Blog Posts
- 1 of 14
- ››
Social Media
- RedBottleDesign W'hoo! Love the new RedBottle Design Office. t.co/LWvEcn1R
- RedBottleDesign t.co/FNwqApQi t.co/FapBkC21
- RedBottleDesign We have a great feeling about 2012... stay tuned!
- RedBottleDesign RedBottle Design wishes everyone a safe and happy Holiday Season.
- RedBottleDesign New blog entry: Java-inspired Enumerated Types in PHP - t.co/Mzix0Vh0
