Expressive, type-checked constants (aka Enums) for PHP
In PHP, class constants can only be defined using expressions that can be evaluated at compile-time. So, in practice, they are almost always either of the string or int type. In this blog post, I would like to explain which drawbacks this brings and how a more robust and expressive software design can be achieved by using object instances instead.
Published on:2019-09-03
In this post
In order to have an example, assume we are dealing with some kind of Order
class that can have a "normal" or "high" order priority status like so:
class Order
{
const PRIORITY_NORMAL = 'normal';
const PRIORITY_HIGH = 'high';
// ...
}
A method accepting one of these constants usually looks like this:
class Order
{
/**
* Create a new order instance
*/
public static function place(ItemList $items, string $priority): self
{ ... }
}
Problems with the scalar-typed approach
Although there are two special string values defined as the two constant values in the above example, clients of this method may pass in literal string values (a plain 'normal'
or 'high'
) directly. For example, this may happen if they are taking the value directly from request parameters, which obviously is a risky practice.
In general, as the Order::place()
method above accepts a string, nothing prevents you from passing in arbitrary string values.
Another possible mistake is that parameter order is messed up and arbitrary values are passed in places where constant values are expected.
To make your software design robust, defensive and fail-fast, in the above example you should check that the $priority
is indeed one of the available priorities.
As you can imagine, this can quickly get impractical when the constant values are used a lot and passed on between methods, as each method would need to check them again and again. Adding a new constant value would also require you to update all those checks.
When working with int
constants ( 0, 1, 2, ...
), the mistake of messing up the parameter ordering might be so subtle that you cannot catch it by checking the parameter value.
Constant value objects
So, here is another way how we can design this. I will be building upon two OOD concepts:
Value objects describes the approach of modelling plain values as objects.
Often, it is benefical as well to design such objects as being immutable.
You can find valuable chapters on both ideas in Eric Evans' book "Domain Driven Design".
Bringing both together, we can come up with an OrderPriorty
class as follows. Let's call this a constant value class:
class OrderPriority
{
private const NORMAL = 'normal';
private const HIGH = 'high';
private $priority;
public static function NORMAL(): self
{
return new self(self::NORMAL);
}
public static function HIGH(): self
{
return new self(self::HIGH);
}
private function __construct($priority)
{
$this->priority = $priority;
}
}
As the OrderPriority
constructor is private, the only way of creating OrderPriority
instances is through the static construction methods OrderPriority::NORMAL()
and OrderPriority::HIGH()
.
We can now write our method as
class Order
{
public static function place(ItemList $items, OrderPriority $priority): self
{ ... }
Since PHP is type-checking the $priority
parameter, you can now be sure you're dealing with some kind of OrderPriority
. Without further checks or safeguards, the place(...)
method can now be sure that $priority
is one of the valid priority levels.
There is one required change for clients passing in one of these values: Instead of writing Order::PRIORITY_NORMAL
or Order::PRIORITY_HIGH
, we will now use OrderPriority::NORMAL()
or OrderPriority::HIGH()
. This is necessary to create the instances of our constant value class.
Note that I chose to name these methods all caps to resemble the convention of constant identifier naming.
Checking for constant values
When checking a parameter like $priority
for one of the constant values, we can write the check as $priority == OrderPriority::HIGH()
. This already works for the ==
loose comparison since the values of both constant class instances – the one in $priority
and the one created on the fly – are the same.
As a cautious programmer, however, you're probably using the strict comparison operator ===
whenever possible. With a constant value class as shown above, this will not work, since this operator also checks for object identity and we're using two different object instances.
So, let's fix that.
Using flyweights
The Flyweight is a structural pattern from the classic "Gang of Four" book. Part of the pattern is the idea to re-use object instances when they don't differ in state, which is perfect for our immutable constant values.
Also described in the pattern is the need to have some kind of factory to obtain flyweight object instances without creating them again and again.
So, let's add a new method to our class that takes care of this. We'll call it constant()
since it returns the constant value object instance for a given value.
This method will be private
as well, so clients still have to use the construction methods ( OrderPriority::NORMAL()
and OrderPriority::HIGH()
) as before.
class OrderPriority
{
private const NORMAL = 'normal';
private const HIGH = 'high';
private static $instances = [];
private $priority;
public static function NORMAL(): self
{
return self::constant(self::NORMAL);
}
public static function HIGH(): self
{
return self::constant(self::HIGH);
}
private static function constant($value)
{
return self::$instances[$value] ?? self::$instances[$value] = new self($value);
}
private function __construct($priority)
{
$this->priority = $priority;
}
}
Now, for every different constant value, only one single object instance will be created. The same instance will be returned for every call to methods like OrderPriority::NORMAL()
. And since there is only one instance per value, the ===
strict comparison now works.
Constant definitions in interfaces
One tiny drawback of the approach shown above is that you cannot put such constant definitions into interfaces. The approach is based on object instances, which are a runtime concept. Interfaces, however, do not contain implementation code and thus cannot provide the necessary methods.
You can, of course, have a "constant value class" to provide the allowed values and then have your interface use it, for example as part of method signatures.
A nice trait
In our current draft of the OrderPriority
class, everything besides the actual two constant values and the construction methods is pretty much boilerplate and would be the same for every "constant value class". So, let's move that to a reusable trait:
trait ConstantClassTrait
{
private static $instances = [];
private $value;
final private function __construct($value)
{
$this->value = $value;
}
private static function constant($value)
{
return self::$instances[$value] ?? self::$instances[$value] = new self($value);
}
}
With this, OrderPriority
becomes:
class OrderPriority
{
use ConstantClassTrait;
private const NORMAL = 'normal';
private const HIGH = 'high';
public static function NORMAL(): self
{
return self::constant(self::NORMAL);
}
public static function HIGH(): self
{
return self::constant(self::HIGH);
}
}
Casting to and from strings
Sooner or later, you might find yourself in a situation where you need to render an HTML form with something like a select list or radio button for an OrderPriority
. Or, you need to accept the OrderPriorty
from a form or the command line as input.
Since we're now dealing with the OrderPriority
class, we now have a perfect place to keep such additional methods:
class OrderPriority
{
// Omitted trait and construction methods (like before)
public static function fromString(string $value): self
{
if ($value !== self::NORMAL && $value !== self::HIGH) {
throw new InvalidArgumentException();
}
return self::constant($value);
}
public function __toString()
{
return $this->value;
}
}
You can now easily cast an OrderPriority
instance to a string, for example when using it as the <input value="...">
.
Now assume your task is to write the code that accepts a new Order
from a web request or the command line. At your UI/code boundary, the order priority will clearly be available as a string. The Order::create()
method, however, needs an OrderPriority
instance.
Even somebody new to your project will quickly figure out that there are only a few ways to actually create OrderPriority
instances. They will probably find the OrderPriority::fromString()
method and write code like this:
// Somehow obtain $itemList
$order = Order::place($itemList, OrderPriority::fromString($_REQUEST['priority']));
// ...
The bottom line is that the way we have written our Order
and OrderPriority
classes here makes it almost impossible to use them in a wrong way or to forget checking our inputs.
Even more value object perks
For a moment, assume your business comes up with a requirement to add a new expedited
priority class. This service level is above the normal
level, but does not yet make a high
priority order.
I'll leave it to you as an exercise to add the EXPEDITED
constant definition, a creation method and to update the fromString()
method in our OrderPriority
class.
What is more interesting is that we can now add an additional method to compare two priorities:
class OrderPriority
{
// ... as before
public function atLeast(OrderPriority $other): bool
{
// Return true if $this->value is a priority equal to or above $other->value.
}
}
With this, our new business rule might be something along the lines of
class ShippingFeeCalculator
{
public function computeShippingFees(Order $order): Money
{
// ...
if ($order->getPriority()->atLeast(OrderPriority::EXPEDITED()) {
// ... do what business requested
}
}
}
Being a value object, the OrderPriority
class is a nice place to keep such additional comparison and computation methods which make it even more expressive.
Summary
This article described how simple class constants can be replaced with instances of a "constant value class". Methods that accept such constants can use type hints to make sure only valid constant values can be passed, without having to perform any additional checks. By reusing object instances, comparisons of constant values can also be written using the ===
strict syntax check, with only minimal changes to code being necessary.
In addition to that, the constant value objects are a great place for keeping a particular kind of business logic.
Sie möchten ein PHP-Projekt realisieren?
Als PHP Agentur haben wir langjährige Erfahrung in der Umsetzung von Softwareprojekten in PHP. Wenn Sie ein Projekt in PHP realisieren wollen und einen Partner suchen, sprechen Sie uns gerne an!