DEV Community

Stefano Fago
Stefano Fago

Posted on

Designing with PHP 8.1 Enumerations

PHP 8.1: The Rise of Enumeration

PHP 8.1 introduces the syntax for the Enumeration. I was curious about the argument and before PHP 8.1 there were more experimental implementations but no direct support. Now Enumeration is here and is a native construct that makes code much more expressive and gives a new way to model software design.

The Enumeration in PHP now has an implicit family where the basic UnitEnum with a minimal contract:

interface UnitEnum
{
  /* Functions */
  public static cases(): array
}
Enter fullscreen mode Exit fullscreen mode

The BackedEnum extends the UnitEnum making it possible to relate primitive values (int or string) to enumeration instances.

interface BackedEnum extends UnitEnum
{ 
   /* Functions */
   public static from(int|string $value): static
   public static tryFrom(int|string $value): ?static

   /* Inherited Functions */
   public static UnitEnum::cases(): array
}
Enter fullscreen mode Exit fullscreen mode

Note:
These elements are interfaces but not in DEV Space since are used by the PHP Engine, introduced for type checking.

I've always seen Enumeration as Domain Symbols ( a limited set of symbols) but the PHP RFC is enough to define some use cases that can be useful to express more design.

PHP Enumeration: some Anti-Pattern!

Before continuing, I want to present some bad ideas in using Enumerations but remain at a high level of abstraction.

The Confusing Constant

It's possible to declare some constants inside an enumeration but the declared constant is not a case of the enumeration. It can be useful to have some edge values inside the enumeration but we need, however, remember to avoid confusing the Devs. Consider also that constants can hide callables!

enum Size
{
    case Small;
    case Medium;
    case Large;


    public const Huge = self::Large;
    public const Max_For_Large_Size = 100;
    public const Cases = ['cases',Size::class];
}

...

var_export(Size::Huge); // alias for Size::Large
var_export(Size::Max_For_Large_Size); // 100
var_export(Size::Cases()); // array (0 => Size::Small,1 =>Size::Medium,2 => Size::Large,)

Enter fullscreen mode Exit fullscreen mode

The Empty Enumeration

It's possible to write an enumeration without any case ( also backed )

enum TheEmptyEnum: int {} 
Enter fullscreen mode Exit fullscreen mode

So we have TheEmptyEnum::cases as an empty array while, in our example, no instances are available, and TheEmptyEnum::tryFrom returns null!

The Commodity Enumeration

Since Enumeration can host function and since the Empty Enumeration, it's possible to write an enumeration that plays the role of a Commodity Class.

enum Calculator
{
    static function sum(int $a,int $b):int
    {
        return $a+$b;
    }
}


Calculator::sum(1,2); // 3 

Enter fullscreen mode Exit fullscreen mode

The Singleton Anti-Pattern(?)

It is possible to use Enumeration to define the concept of the Singleton.

enum Runtime
{
    case INSTANCE;
    function elapsed():float
    {
        ...
    }   
}
...
Runtime::INSTANCE->elapsed(); 
Enter fullscreen mode Exit fullscreen mode

Enumerations, Template & Constant Expression

Nowadays, enumeration can't be defined as generics by using docblock template and also if a case can be used as constant, it can't be some element coming from the enumeration function.

Interesting Design Aspect of Enumerations

We can have the following interesting usage of enumeration remembering always the concept that they are enumerable elements.

The Interface Friend

An enumeration can implement an interface; it's interesting to notice that, more than the obligation of the contract, we obtain protection from the fact that what we consider enumerable today, tomorrow will be not but we don't need to change the signature of the functions.

The Trait Friend

An enumeration is a more constrained environment and using traits can be a way to enhance some enumeration abilities, coming from the new PHP 8.X features, as we can see in these projects:

The Enumeration State Machine

A state machine has an enumerable set of states so it's possible to use the new RFC and a UnitEnum to define a functioning State Machine


enum PeachesPipe
{
    case WASH;
    case PEEL;
    case CUT;
    case PUT_IN_JAR;
    case READY;

    function process():PeachesPipe
    {
      return [$this,$this->name]();
    }


    private function WASH():PeachesPipe
    {
        echo $this->name.PHP_EOL;
        return PeachesPipe::PEEL;
    }

    private function PEEL():PeachesPipe
    {
        echo $this->name.PHP_EOL;
        return PeachesPipe::CUT;
    }

    private function CUT():PeachesPipe
    {
        echo $this->name.PHP_EOL;
        return PeachesPipe::PUT_IN_JAR;
    }

    private function PUT_IN_JAR():PeachesPipe
    {
        echo $this->name.PHP_EOL;
        return PeachesPipe::READY;
    }
}
...
$pipeline = PeachesPipe::WASH;

while($pipeline !== PeachesPipe::READY)
{
    $pipeline = $pipeline->process();
}
// WASH
// PEEL
// CUT
// PUT_IN_JAR

Enter fullscreen mode Exit fullscreen mode

The Enumeration Strategy Pattern

We can use an enumerable set of algorithms related to a specific Domain, defining related strategies inside an enumeration.


enum Currency
{
    case EUR;
    case USD;
}


final class Item implements Stringable
{
  function __construct(public readonly string $id, 
               public readonly int $quantity, 
               public readonly int $price, 
               public readonly Currency $currency){}

   function __toString():string
   {
       return '[ id:'.$this->id.
              ', quantity: '.$this->quantity.
              ', price:'.$this->price.' '.$this->currency->name.' ]';
   }
}


final class Basket implements Stringable
{
    /** @param list<Item> $items **/
   function __construct(private array $items){}

    /** @return list<Item> **/
   function items():array
   {
      return $this->items; 
   }

   function __toString():string
   {
       return '(Basket: '.var_export($this->items,true).')';
   }
}


enum DiscountType: int
{
    case BRONZE=5;
    case SILVER=10;
    case GOLD=15;

    static function apply(Basket $basket, DiscountType $discount):Basket
    {
        $factor = $discount->value/100;
        $items = array_map(
                   fn(Item $item)=>
                         new Item($item->id,
                                  $item->quantity,
                                  ($item->price - (int)($item->price*$factor)),
                                  $item->currency),
                  $basket->items()
                );
       return new Basket($items);
    }

}


$items = array(
       new Item('az123',2,100,Currency::EUR),
       new Item('azBCV',5,200,Currency::EUR),
       new Item('azKJI',1,150,Currency::EUR),
    );

$basket = new Basket($items);
$basket = DiscountType::apply($basket,DiscountType::BRONZE); 

Enter fullscreen mode Exit fullscreen mode

The Enumeration Factory Pattern

An enumerable set of elements can be created from a function hidden in a specific backed enumeration


final class Topping implements Stringable
{
    function __construct(public readonly string $name){}

    function __toString():string
    {
        return 'Topping('.$this->name.')';
    }
}


final class Cake implements Stringable
{
    function __construct(public readonly CakeType $type, 
                         public readonly Topping $top, 
                         public readonly int $weight){}

    function __toString():string
    {
        return 'Cake('.$this->type->name.','.$this->top.', '.$this->weight.')';
    }
}

interface CakeFactory
{
    function create():Cake;
}

enum CakeType: int implements CakeFactory
{
    case CHOCOLATE=2000;
    case VANILLA=2200;
    case FROSTED=2500;

    function create():Cake
    {
        return match($this)
        {
         CakeType::CHOCOLATE=>new Cake($this,new Topping('Glazed Choco'),$this->value),
         CakeType::VANILLA=>new Cake($this,new Topping('Glazed White'),$this->value),
         CakeType::FROSTED=>new Cake($this,new Topping('Cherries'),$this->value),
        };
    }
}

...
echo CakeType::VANILLA->create(); // Cake(VANILLA,Topping(Glazed White), 2200)

Enter fullscreen mode Exit fullscreen mode

It's also possible to introduce some laziness using closure instead of simple functions and playing with the concept of private const (callable) and/or supplier closure.

The Processor Set Pattern

An enumerable set of processors can be expressed thank to an enumeration also using a touch of FP.


enum Operation: int
{
   case SUM=1;
   case SUB=2;
   case MUL=3;
   case DIV=4;

   /** @return callable(Operation):callable(int,int):int **/
   static function processorsMap():callable
   {
      $processors = array(
        Operation::SUM->value =>fn(int $a, int $b):int=>$a+$b,
        Operation::SUB->value =>fn(int $a, int $b):int=>$a-$b,
        Operation::MUL->value =>fn(int $a, int $b):int=>$a*$b,
        Operation::DIV->value =>fn(int $a, int $b):int=>(int)($a/$b),
      );

      /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ 
     return fn(Operation $opKey)=>$processors[$opKey->value];
   }
}
...
$processors = Operation::processorsMap();
echo $processors(Operation::SUM)(1,3); // 4

Enter fullscreen mode Exit fullscreen mode

The Conditional Processor Pattern

We can have an enumerable set of flags conditioning a specific processing operation and we can use an enumeration for that.


enum EventType: int
{
  case CLICKED=1;
  case FOCUSED=2;
  case EXECUTED=4;
  case BLURRED=8;

  static function eventsCombining(EventType ... $type): int
  {
     $values = array_map(fn(EventType $eventType)=>$eventType->value,$type);
     return (int) array_reduce($values, 
                               /** 
                                * @param int|null $carry
                                * @param int $val
                                * @return int */ 
fn($carry,$val)=> $carry===null?$val: ($carry | $val)); 
  }
}
...
echo 'events set: '.EventType::eventsCombining(
                        EventType::CLICKED,  
            EventType::FOCUSED,
                    EventType::EXECUTED,
                    EventType::BLURRED)
                     .PHP_EOL; // events set: 15
Enter fullscreen mode Exit fullscreen mode

An interesting and more complex example comes from this project:

The Enumeration Random Generator

It's also possible to extract randomly from enumeration instances as follows:


 /**
  * @template T of \UnitEnum
  * @param class-string<T> $enumType
  * @return callable():T
  **/
function randomEnumGenerator(string $enumType):callable
{
 /** @var array<T> $cases **/ $cases = [$enumType,"cases"]();
 $len = count($cases)-1;
 return fn()=> $cases[rand(0,$len)];
};

enum Direction
{
    case North;
    case South;
    case East;
    case West;
}
...
$gen = randomEnumGenerator(Direction::class);
var_export( $gen()); // Direction::West

Enter fullscreen mode Exit fullscreen mode

Note: Here are used Template e Template Class to force typing

To Go Further

More are not happy with this kind of design but I hope You can find some useful use cases. We need always to use some discipline, and remember the real meaning of an enumeration!

Top comments (0)