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.

#define BINARY       1
#define OCTAL        2
#define DECIMAL      3
#define HEXADECIMAL  4

int numericBase = BINARY;

Selecting an item from a set using C macros

enum NumericBase { BINARY, OCTAL, DECIMAL, HEXADECIMAL };

enum NumericBase numericBase1 = BINARY;
int              numericBase2 = BINARY;

Selecting an item from a set using C enumerations

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?

int numericBase = FLOATING_POINT;

Uncaught code bug

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.

function printNumber( int $numericBase ) { }

printNumber( FLOATING_POINT );

Uncaught code bug

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.

PhpStorm 2020.1 hinting you at an error

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!

Photo by Geran de Klerk on Unsplash