Smart PHP Enums
PHP lacks an enumeration type. Can you achieve the same with classes, while retaining the advantages of enums?
5 min read
A software developer will eventually need to write code that selects an item from a set: a numeric base, such as Binary, from a set of numeric bases; a numeric precision, such as Floating-Point; a network protocol, such as TCP.
Computers however, are little more than elaborate pocket calculators. They are great with numbers and little else. So software developers assign numbers to each of the items in their sets: 1 for the first item, 2 for the second and so on, for instance. They can now express their selection in code, as can be seen below.
int x = 1;
This can quickly become a problem. When you come back to this code a few weeks
later, does this tell you (the software developer) your intent?
What is x
and from which set was that 1
taken from:
numeric bases, precisions or network protocols?
The first fix is easy to do: rename your variable.
int numericBase = 1;
But now you need to provide meaning to your 1
.
Your intent is still not clear in your code, and your development tools (IDE, compiler)
can't help you because you're just assigning a random number to an integer variable.
Back in the days, you would address this issue in C with a macro definition
or enumeration, abbreviated enum
.
Your code is now clearer, but you still have issues.
In C,
BINARY
is still really just an integer,
so what's preventing you from, by mistake, assigning say 200
to that variable?
What if you create other macros, for other sets, and mix them up?
Which macros should be valid in this assignment? Your IDE and compiler can't help, because you're still assigning an integer to a variable.
Enumeration as a Type
Some languages, such as C++ and C# have addressed this by elevating enumerations to their own types. You can now have your own data type, rather than just integer, and specify which values are allowed for this type.
enum NumericBase { BINARY, OCTAL, DECIMAL, HEXADECIMAL } NumericBase numericBase = NumericBase.BINARY;
Your code now looks much better.
It is also type-safe, meaning that your IDE and compiler can help you.
Variable numericBase
can only hold values of type NumericBase
,
so you can no longer assign it 200
, or a value from a different
enumeration.
Enumerations in PHP
PHP, at least up to version 7.4 at the time of this writing, doesn't support enumerations. It's common to address this limitation using two separate techniques.
The first is similar to C's macros.
define( "BINARY", 1 ); define( "OCTAL", 2 ); define( "DECIMAL", 3 ); define( "HEXADECIMAL", 4 ); $numericBase = BINARY;
The second is similar to C's enumerations, but using class constants.
class NumericBase { public const BINARY = 1; public const OCTAL = 2; public const DECIMAL = 3; public const HEXADECIMAL = 4; } $numericBase = NumericBase::BINARY;
Neither of these two solutions address the fact that we're just assigning random integers to variables, and hence the IDEs and interpreter can't help us, and the following function call is still possible.
Can we be smarter?
Smart PHP Enums
The following class acts as a PHP enumeration.
/** * Class NumericBase acting as an enumeration * Built by NumericBase::BINARY(), ... */ class NumericBase { protected $base; protected function __construct(int $base) { $this->base=$base; } // Factory of constants public static function BINARY() : self { return new self(2); } public static function OCTAL() : self { return new self(8); } public static function DECIMAL() : self { return new self(10); } public static function HEXADECIMAL() : self { return new self(16); } }
This is a lightweight enumeration-like class in PHP, a simplified version of what you may find elsewhere. You assign an enumeration constant to a variable as follows.
$numericBase = NumericBase::BINARY();
This is type-safe, so you can only assign “constants” from this class to typed parameters/properties of the same type.
It is serializable/unserializable with a small payload size.
The following code outputs
O:11:"NumericBase":1:{s:7:"*base";i:2;}
print_r( serialize(NumericBase::BINARY()) );
You can determine the value of an enumeration variable/property as you would in other languages.
if ($numericBase == NumericBase::BINARY()) echo "I'm binary!";
switch ($numericBase) { case NumericBase::BINARY(): // ... case NumericBase::OCTAL(): // ... case NumericBase::DECIMAL(): // ... case NumericBase::HEXADECIMAL(): // ... }
But because this is a class rather than an enum
you can also add
auxiliary methods. If they are small enough, they add negligible JIT compilation
delay and code complexity.
/** * Class NumericBase acting as an enumeration * Built by NumericBase::BINARY(), ... */ class NumericBase implements JsonSerializable { protected $base; protected function __construct(int $base) { $this->base=$base; } // Factory of constants public static function BINARY() : self { return new self(2); } public static function OCTAL() : self { return new self(8); } public static function DECIMAL() : self { return new self(10); } public static function HEXADECIMAL() : self { return new self(16); } // Optional auxiliary methods public function asInteger() : int { return $this->base; } public function isBinary() : bool { return $this->base === 2; } public function isOctal() : bool { return $this->base === 8; } public function isDecimal() : bool { return $this->base === 10; } public function isHexadecimal() : bool { return $this->base === 16; } public function jsonSerialize() : array { return get_object_vars($this); } }
You can now determine the value with a shorthand, without creating new temporary objects.
if ($numericBase->isBinary()) echo "I'm binary!";
This class is now JSON-encodable. The following code outputs
{"base":2}
print_r( json_encode(NumericBase::BINARY()) );
The underlying integer values for each enumeration value were chosen as to be useful
on their own, so there's an auxiliary $numericBase->asInteger()
.
You cannot however, create instances of this enumeration with random underlying integer
values, because there is no such method. In fact, the underlying $base
,
as well as the class' constructor itself are protected
preventing them
from being used freely except by derived classes.
This is all by design.
Happy coding!