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
}
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
}
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,)
The Empty Enumeration
It's possible to write an enumeration without any case ( also backed )
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.
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.
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
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.
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
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.
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.
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:
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
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
Top comments (0)