enums
The Missing Data Type
Embracing
Slides/Code/Resources
Rate This Talk
joind.in/talk/a3270
wkdb.yt/enums
Hello, World...
wkdb.yt/enums
- Contract PHP Developer and Consultant
- Currently Living in Dallas w/ Wife and Dog
- Never Intended to Be a Developer
enums
The Missing Data Type
enums
The Missing Data Type
Embracing
What Does It Mean To Be Enumerable?
Things that Exist in Known, Limited, and Discrete States
wkdb.yt/enums
What Does It Mean To Be Enumerable?
Enum values are important to us because of what they represent and in context to other values in the set
UserRole |
---|
admin |
manager |
user |
guest |
InvoiceType |
---|
standard |
prorated |
comped |
CardBrand |
---|
visa |
mastercard |
discover |
amex |
wkdb.yt/enums
ProcessING a Card Payment
A payment result will have only one of three values:
APPROVED
DECLINED
ERROR
wkdb.yt/enums
ProcessING a Card Payment
APPROVED
DECLINED
ERROR
wkdb.yt/enums
public function processPayment(): string
{
$result = $this->payment_gateway->run();
if ($result) {
if ($result->approval_code) {
return 'approved';
}
return 'declined';
}
return 'error';
}
PaymentProcessor
PROCESSING A CARD PAYMENT
"Magic Strings"
wkdb.yt/enums
public function setResult(string $result): void
{
$result = \trim(\strtolower($result));
in_array($result, [
'approved',
'declined',
'error',
], true) || throw new UnexpectedValueException();
$this->result = $result;
}
PaymentModel
PROCESSING A CARD PAYMENT
"Magic Strings"
wkdb.yt/enums
public function processPayment()
{
$result = $this->gateway->run();
if ($result) {
if ($result->approval_code) {
return 1;
}
return 2;
}
return 0;
}
PaymentProcessor
PROCESSING A CARD PAYMENT
"Magic Integers"
wkdb.yt/enums
class PaymentResult
{
public const APPROVED = 'approved';
public const DECLINED = 'declined';
public const ERROR = 'error';
}
PaymentResult
PROCESSING A CARD PAYMENT
Class Constants
wkdb.yt/enums
public function processPayment()
{
$result = $this->gateway->run();
if ($result) {
if ($result->approval_code) {
return PaymentResult::APPROVED;
}
return PaymentResult::DECLINED;
}
return PaymentResult::ERROR;
}
PaymentProcessor
PROCESSING A CARD PAYMENT
Class Constants
wkdb.yt/enums
PaymentModel
PROCESSING A CARD PAYMENT
Class Constants
public function setResult(string $result): void
{
$result = \trim(\strtolower($result));
in_array($result, [
PaymentResult::APPROVED,
PaymentResult::DECLINED,
PaymentResult::ERROR,
], true) || throw new UnexpectedValueException();
$this->result = $result;
}
wkdb.yt/enums
So, What's The Problem ?
Class Constants help with code comprehension and centralization.
They are just syntactic sugar around scalars.
wkdb.yt/enums
So, What's The Problem ?
class AccountStatus
{
public const APPROVED = 'approved';
public const ACTIVE = 'active';
public const DEACTIVATED = 'deactivated';
}
AccountStatus
$result = $this->gateway->run($transaction);
if($result->status === 201){
$transaction->setResult(AccountStatus::APPROVED)
}
This works, but it is oh, so wrong...
wkdb.yt/enums
So, What's The Problem ?
Scalar Type Definitions reduce type errors.
They do not improve readability.
Working directly with scalars allows illogical operations.
wkdb.yt/enums
class WeekDay
{
public const MONDAY = 0;
public const TUESDAY = 1;
public const WEDNESDAY = 2;
public const THURSDAY = 3;
public const FRIDAY = 4;
public const SATURDAY = 5;
public const SUNDAY = 6;
}
WeekDay::SATURDAY + WeekDay::SUNDAY === 11;
So, What's The Problem ?
This is perfectly legal...
wkdb.yt/enums
So, What's The Problem ?
Validation reduces runtime errors.
Only applies to the current context.
Cannot trust a scalar value to not change.
wkdb.yt/enums
What Properties Does an EnumeratION Need to Preserve Context Across Code BOUNDRIES?
wkdb.yt/enums
Properties of An Ideal Enumeration
Immutable
wkdb.yt/enums
Self-Valid
Properties of An Ideal Enumeration
wkdb.yt/enums
Comparable
PaymentResult::APPROVED() == PaymentResult::APPROVED();
'approved' === PaymentResult::APPROVED()->value();
Properties of An Ideal Enumeration
wkdb.yt/enums
Serializable
'approved' === (string) PaymentResult::APPROVED();
PaymentResult::make('approved') == PaymentResult::APPROVED();
Properties of An Ideal Enumeration
wkdb.yt/enums
This Sounds Familiar...
Value Object
"A small simple object, like money or a date range, whose equality isn't based on identity."
"...I follow a simple but important rule: value objects should be immutable"
-Martin Fowler
wkdb.yt/enums
<?php
declare(strict_types=1);
namespace App\Cached;
use UnexpectedValueException;
abstract class Enum
{
protected $value; // The underlying scalar value
final private function __construct(string $enum)
{
$enum = strtoupper($enum);
if (!array_key_exists($enum, static::$values)) {
throw new UnexpectedValueException();
}
$this->value = static::$values[$enum];
}
public static function __callStatic($enum, $args): self
{
return static::$cache[$enum] ??= new static($enum);
}
public function __set($name, $value): void
{
throw new \LogicException("Read Only");
}
// Make an instance from an underlying scalar value
public static function make($value): self
{
$enum = array_search($value, static::$values, true);
if ($enum === false) {
throw new UnexpectedValueException();
}
return static::$cache[$enum] ??= new static($enum);
}
// Return the underlying scalar value
public function value()
{
return $this->value;
}
// Allow an instance to be cast to a string of the value
public function __toString(): string
{
return (string)$this->value;
}
public function is($value): bool
{
if ($value instanceof static) {
return $this->value === $value->value;
}
return $this->value === $value;
}
}
Define a Self-Valid, Immutable Parent Class...
wkdb.yt/enums
<?php
declare(strict_types=1);
namespace App;
/**
* @method static PaymentResult APPROVED()
* @method static PaymentResult DECLINED()
* @method static PaymentResult ERROR()
*/
final class PaymentResult extends Enum
{
protected static $values = [
'APPROVED' => 'approved',
'DECLINED' => 'declined',
'ERROR' => 'error',
];
}
Defining the Child Class...
wkdb.yt/enums
public function processPayment(): PaymentResult
{
$result = $this->gateway->run();
if ($result) {
if ($result->approval_code) {
return PaymentResult::APPROVED();
}
return PaymentResult::DECLINED();
}
return PaymentResult::ERROR();
}
Using the Enum: Updating Our Code
public function setResult(PaymentResult $result): void
{
$this->result = $result->value();
}
wkdb.yt/enums
<?php
namespace App\Services\Reporting\KpiReporting;
use MyCLabs\Enum\Enum;
/**
* @method static KpiReportPeriod CURRENT()
* @method static KpiReportPeriod DAILY()
* @method static KpiReportPeriod MONTH_TO_DATE()
* @method static KpiReportPeriod QUARTER_TO_DATE()
* @method static KpiReportPeriod YEAR_TO_DATE()
*/
class KpiReportPeriod extends Enum
{
private const CURRENT = 'current';
private const DAILY = 'daily';
private const MONTH_TO_DATE = 'month_to_date';
private const QUARTER_TO_DATE = 'quarter_to_date';
private const YEAR_TO_DATE = 'year_to_date';
}
MyCLabs\Enum Package
wkdb.yt/enums
So, What's The Problem ?
wkdb.yt/enums
Caching !== Comparison by Identity
$result = PaymentResult::APPROVED();
$serialized = serialize($result);
var_dump($result, unserialize($serialized));
object(App\Cached\PaymentResult)#388 (1) {
["value":protected]=>
string(8) "approved"
}
object(App\Cached\PaymentResult)#360 (1) {
["value":protected]=>
string(8) "approved"
}
PHP's Enumerated Data Type
*
wkdb.yt/enums
enum PaymentResult
{
case APPROVED;
case DECLINED;
case ERROR;
}
What is an Enumerated Type?
A type is an attribute of a unit of data that tells the computer what values it can take and what operations can be performed upon it.
PHP Types |
---|
boolean |
integer |
float |
string |
array |
object |
callable |
iterable |
resource |
NULL |
What is a Type?
wkdb.yt/enums
wkdb.yt/enums
What is an Enumerated Type?
An enumerated type is a named, language-level construct that restricts the possible values of an instance to a discrete set of named members, called enumerators, at compile time.
In practice, this means that an enum variable can have any value in the set and only values in the set can be values.
wkdb.yt/enums
Degenerate Enumerations
The Enumerated Type PHP Does Have...
enum boolean {
false = 0,
true = 1
};
wkdb.yt/enums
enum unit_type {
null = 0,
};
null
bool
wkdb.yt/enums
TWo Types of Enums
enum ChessPiece
{
case PAWN;
case KNIGHT;
case BISHOP;
case ROOK;
case QUEEN;
case KING;
}
wkdb.yt/enums
Pure Enum
TWo Types of Enums
ChessPiece::KNIGHT->name === 'KNIGHT'; // true
ChessPiece::KNIGHT === ChessPiece::KNIGHT; // true
ChessPiece::cases() === [
0 => ChessPiece::PAWN,
1 => ChessPiece::KNIGHT,
2 => ChessPiece::BISHOP,
3 => ChessPiece::ROOK,
4 => ChessPiece::QUEEN,
5 => ChessPiece::KING,
];
wkdb.yt/enums
Pure Enum
TWo Types of Enums
enum CardSuit: string
{
case CLUBS = '♣';
case DIAMONDS = '♦';
case HEARTS = '♥';
case SPADES = '♠';
}
wkdb.yt/enums
Backed Enum
TWo Types of Enums
CardSuit::HEARTS->name === 'HEARTS'
CardSuit::HEARTS->value === '♥'
CardSuit::values() === [
'CLUBS' => '♣',
'DIAMONDS' => '♦',
'HEARTS' => '♥',
'SPADES' => '♠',
];
/** @throws ValueError */
CardSuit::try(string|int $value): CardSuit
CardSuit::tryFrom(string|int $value): CardSuit|null
wkdb.yt/enums
Backed Enum
wkdb.yt/enums
PHP ENUMS BEHAVE LIKE OBJECTS
- Can declare public/protected/private static and non-static methods
- Can declare public/protected/private class constants
- Can implement arbitrary interfaces (including
ArrayAccess
) - Can use arbitrary traits that only define static and non-static methods
- Safely serialize/deserialize with
serialize()
andigbinary_serialize()
- Backed enums serialize to their value when transformed with
json_encode()
- Cannot be used as an array key
-
Cannot be cast to scalar value with
(string)
or(int)
operators
wkdb.yt/enums
Except When They Don't
- Cannot be cloned
- Cannot declare static or object properties
- Cannot declare
__construct
() or__destruct()
methods - Cannot implement magic methods except
__call()
__callStatic()
__invoke()
Enums are Stateless* Singletons
wkdb.yt/enums
Except When They Don't
enum CardSuit: string implements \Countable
{
case CLUBS = '♣';
case DIAMONDS = '♦';
case HEARTS = '♥';
case SPADES = '♠';
public function count(): int
{
static $counter = 0;
return ++$counter;
}
}
var_dump(count(CardSuit::CLUBS));
var_dump(count(CardSuit::SPADES));
var_dump(count(CardSuit::SPADES));
var_dump(count(CardSuit::HEARTS));
Statelessness Doesn't Apply to Method Scope
wkdb.yt/enums
Except When They Don't
Statelessness Doesn't Apply to Method Scope
wkdb.yt/enums
Except When They Don't
Inheritance is Prohibited
If an enumeration could be extended, it would not be an enumeration!
enum TrafficLight
{
case RED;
case YELLOW;
case GREEN;
}
enum WeirdLight extends TrafficLight
{
case BLUE;
}
ENUMS Can Be Aliased
enum ChessPiece
{
public const self HORSEY = self::KNIGHT;
case PAWN;
case KNIGHT;
case BISHOP;
case ROOK;
case QUEEN;
case KING;
}
\assert(ChessPiece::HORSEY->name === 'KNIGHT'); // true
wkdb.yt/enums
ENUMS Can Be Callable
enum ChessPiece
{
case PAWN;
case KNIGHT;
case BISHOP;
case ROOK;
case QUEEN;
case KING;
public function __invoke(): int
{
return match($this){
self::KING, self::QUEEN => 1,
self::ROOK, self::BISHOP, self::KNIGHT => 2,
self::PAWN => 8,
};
}
}
wkdb.yt/enums
ENUMS Can Be Callable
wkdb.yt/enums
The Simplest PHP ENUM
enum EmptyEnum
{
}
var_dump(EmptyEnum::cases()); // []
wkdb.yt/enums
This is perfectly legal...
...but probably useless
Unit Type
enum UnitType
{
case NULL;
}
wkdb.yt/enums
Unit Type
function transform(array $values): array
{
$transformed = [
"Package" => $values['package'] ?? null,
"Code" => $values['promo_code'] ?? null,
];
if (isset($values['extra_1'])) {
$transformed['Extra 1'] = $values['extra_1'];
}
if (isset($values['extra_2'])) {
$transformed['Extra 2'] = $values['extra_2'];
}
if (isset($values['users'])) {
$transformed['Extra Users'] = $values['users'];
}
return $transformed;
}
wkdb.yt/enums
Unit Type
function transform(array $values): array
{
return \array_filter([
"Package" => $values['package'] ?? null,
"Code" => $values['promo_code'] ?? null,
'Extra 1' => $values['extra_1'] ?? UnitType::NULL,
'Extra 2' => $values['extra_2'] ?? UnitType::NULL,
'Extra Users' => $values['users'] ?? UnitType::NULL,
], static fn(mixed $value): bool => $value !== UnitType::NULL);
}
wkdb.yt/enums
Enums for Error Handling
enum ValidationError
{
case MISSING_OR_EMPTY;
case OVERFLOW;
case UNDERFLOW;
case INVALID_VALUE;
}
function validate(mixed $value): string|ValidationError
{
//
}
wkdb.yt/enums
Enums for Error Handling
enum ValidationError
{
case MISSING_OR_EMPTY;
case OVERFLOW;
case UNDERFLOW;
case INVALID_VALUE;
}
readonly class ValidatedValue
{
public function __construct(public mixed $value)
{
}
}
function validate(mixed $value): ValidatedValue|ValidationError
{
//
}
wkdb.yt/enums
Enums for Error Handling
interface Validation
{
public function validated(): ValidatedValue|null;
}
enum ValidationError implements Validation
{
case MISSING_OR_EMPTY;
case OVERFLOW;
case UNDERFLOW;
case INVALID_VALUE;
public function validated(): null
{
return null;
}
}
readonly class ValidatedValue implements Validation
{
//
}
wkdb.yt/enums
Enums for Error Handling
function check(mixed $value): mixed
{
$value = validate($mixed);
if($value instanceof ValidationError){
throw new UnexpectedValueException($value->name);
}
return $value->value;
}
wkdb.yt/enums
Enums for Error Handling
function check(mixed $value): mixed
{
return validate($mixed)->validated()->value
?? throw new UnexpectedValueException();
}
wkdb.yt/enums
Slides/Code/Resources
Rate This Talk
joind.in/talk/a3270
Thank You
wkdb.yt/enums
Embracing Enums
By Andy Snell
Embracing Enums
In January 2020, I delivered a conference talk titled "Enums: The Missing Data Type", which ended on a sour note: PHP probably would not have a native type for handling enumerations any time soon. To my surprise and delight, PHP 8.1 would release with a new "Enum" type less than two years later. Now we can really explore enumerations: both the theory behind them and the current PHP implementation. We’ll cover how representing things like statuses with enums improves immutability, readability, and type safety; the different types of enums available to us; and address the most common questions around the current limitations of enums. Resources: https://github.com/andysnell/embracing-enums
- 15