Designing with PHP 8.1 Enumerations
Tags: php, learning, designpatterns, enumeration
Published: 2023-12-03 17:08:36

## PHP 8.1: The Rise of Enumeration

PHP 8.1 introduces the syntax for the [Enumeration](https://www.php.net/manual/en/language.enumerations.php). 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](https://www.php.net/manual/en/class.unitenum.php) with a minimal contract:

```php
interface UnitEnum
{
  /* Functions */
  public static cases(): array
}
```

The [BackedEnum](https://www.php.net/manual/en/class.backedenum.php) extends the UnitEnum making it possible to relate primitive values (int or string) to enumeration instances.

```php
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
}
```
>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*!

```php
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,)

```

### The Empty Enumeration

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

```php
enum TheEmptyEnum: int {} 
```

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*.

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


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

```

### The Singleton Anti-Pattern(?)

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

```php
enum Runtime
{
    case INSTANCE;
    function elapsed():float
    {
        ...
    }   
}
...
Runtime::INSTANCE->elapsed(); 
```

### 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:

- https://github.com/Elao/PhpEnums
- https://github.com/josezenem/php-enums-extended
- https://github.com/dive-be/php-enum-utils

### 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

```php

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

```

### The Enumeration Strategy Pattern

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

```php

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); 

```

### The Enumeration Factory Pattern

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

```php

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)

```
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.

```php

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

```

### 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.

```php

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
```

An interesting and more complex example comes from this project:

- https://github.com/framjet/php-enum-bitmask


### The Enumeration Random Generator

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

```php

 /**
  * @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

```
>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!

- https://aye.sh/files/talks/2021/PHP-Enums-MidwestPHP/PHP-Enums.pdf
- https://github.com/Seldaek/monolog/blob/3.0.0/src/Monolog/Level.php
- https://www.youtube.com/watch?v=5Cgio2OfOYk
- https://stitcher.io/blog/php-enums
- https://www.amitmerchant.com/native-enumerations-are-coming-in-php-81/
- https://goatreview.com/using-php-8-1-enumerations-in-symfony/
- https://bestofphp.com/repo/bpolaszek-doctrine-native-enums-php-miscellaneous