PHP

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.

Written by: Matthias
Published on: 2019-09-03

In this Post

Update 02/2021: Native support for Enums has been added in PHP 8.1 . You can find more details over at Brent's blog. Also, he published another article alongside the announcement, explaining how to get similar results before PHP 8.1. You might want to read both in combination with this article here; my feeling is the techniques complement each other.

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 OrderPriorityinstances. 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!

Interesse geweckt?

Wir hören gerne zu, wenn Sie Fragen oder Anmerkungen zu diesem Thema haben. Und wenn Sie ein Projekt, ein Produkt, ein Problem oder eine Idee mit uns besprechen möchten, freuen wir uns erst recht über ein Gespräch!