enums​

The Missing Data Type

Embracing

Slides/Code/Resources

+ Link to Joind.in

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/Speaker

enums​

The Missing Data Type

enums​

The Missing Data Type

Embracing

PHP B.E.

wkdb.yt/enums

(Before Enums)

ProcessING a Card Payment

A payment result will have only one of three values:

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 processPayment(): int
{
    $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';
}

PROCESSING A CARD PAYMENT

Class Constants

wkdb.yt/enums
class PaymentResult
{
    public const APPROVED = 1;
    public const DECLINED = 2;
    public const ERROR = 0;
}
public function processPayment(): string
{
    $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 AccountStatus
{
    public const APPROVED = 'approved';
    public const ACTIVE = 'active';
    public const DEACTIVATED = 'deactivated';
}
AccountStatus
if($this->gateway->run($transaction)->status === 201){
	$transaction->setResult(AccountStatus::APPROVED)
}

This works, but it is oh, so wrong...

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 ?

Class Constants help with code comprehension and centralization.

They are just syntactic sugar around scalars.

wkdb.yt/enums

So, What's The Problem ?

Scalar Type Definitions reduce type errors, but are poor representations of "things"

 

Working directly with scalars allows illogical operations.

 

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

It's All About Representing Context and Comparability Across Code Boundaries

wkdb.yt/enums
wkdb.yt/enums

There's Always A Relevant XKCD Comic

Enums in Theory

wkdb.yt/enums

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
wkdb.yt/enums

Mercury

Venus

Earth

Mars

Jupiter

Saturn

Uranus

Neptune

Pluto

wkdb.yt/enums

Mercury

Venus

Earth

Mars

Jupiter

Saturn

Uranus

Neptune

Pluto

Enumeration of Planets in Our Solar System

wkdb.yt/enums

Mercury

Venus

Earth

Mars

Jupiter

Saturn

Uranus

Neptune

Pluto

Enumeration of Planets in Our Solar System

Invalid Enumeration

wkdb.yt/enums

Mercury

Venus

Earth

Mars

Jupiter

Saturn

Uranus

Neptune

Pluto

Enumeration of Planets in Our Solar System

Enumeration of Rocky Planets in Our Solar System

wkdb.yt/enums

Mercury

Venus

Earth

Mars

Jupiter

Saturn

Uranus

Neptune

Pluto

Enumeration of Habitable Planets in Our Solar System

wkdb.yt/enums

Mercury

Venus

Earth

Mars

Jupiter

Saturn

Uranus

Neptune

Pluto

Enumeration of Planets in Our Solar System with Intelligent Life

Enums in Practice

wkdb.yt/enums

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

What Properties Does an Enumeration Need to Preserve Context Across Code Boundaries?

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;
PaymentResult::Approved !== PaymentResult::Declined;

PaymentResult::Approved !== AccountStatus::Approved;

Properties of An Ideal Enumeration

wkdb.yt/enums

Serializable

// PHP Serialization
PaymentResult::Approved === \unserialize(\serialize(PaymentResult::Approved));

// Casting/Mutating, e.g. Database
PaymentResult::Approved->value === 'approved';
PaymentResult::from('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

The Ideal VAlue Objects

wkdb.yt/enums

Enums

Domain Objects

PHP Objects

Value Objects

Enums

Degenerate Enumerations

The Two Enums PHP Had Prior to 8.1...

enum boolean {
    false = 0,
    true = 1
};
wkdb.yt/enums
enum unit_type {
    null = 0,
};

null

bool

wkdb.yt/enums

PHP's Enumerated Data Type

wkdb.yt/enums
enum PaymentResult
{    
    case Approved;
    case Declined;
    case Error;
}

TWo Types of Enums: Pure

enum ChessPiece
{    
    case Pawn;
    case Knight;
    case Bishop;
    case Rook;
    case Queen;
    case King;
}

ChessPiece::Knight->name === 'Knight';
ChessPiece::Knight === ChessPiece::Knight;
wkdb.yt/enums

TWo Types of Enums: Pure

interface UnitEnum {
	public static cases(): array
}
wkdb.yt/enums
ChessPiece::cases() === [
    0 => ChessPiece::Pawn,
    1 => ChessPiece::Knight,
    2 => ChessPiece::Bishop,
    3 => ChessPiece::Rook,
    4 => ChessPiece::Queen,
    5 => ChessPiece::King,
];

TWo Types of Enums: Backed

enum CardSuit: string
{    
    case Clubs = '♣';
    case Diamonds = '♦';
    case Hearts = '♥';
    case Spades = '♠';
}

CardSuit::Hearts->name === 'Hearts';
CardSuit::Hearts->value === '♥';
wkdb.yt/enums

TWo Types of Enums: Backed

CardSuit::from('♥') === CardSuit::Hearts;
CardSuit::tryFrom('♥') === CardSuit::Hearts;
CardSuit::tryFrom('☹') === null;

CardSuit::cases() === [
    0 => CardSuit::Clubs,
    1 => CardSuit::Diamonds,
    2 => CardSuit::Hearts,
    3 => CardSuit::Spades,
];
wkdb.yt/enums

Backed Enum

interface BackedEnum extends UnitEnum {
	public static from(int|string $value): static;
	public static tryFrom(int|string $value): ?static;
}

PHP's enums are fancy objects with a lot of limitations.

wkdb.yt/enums

...and that's what makes them useful

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 declare class and method attributes
  • Can use arbitrary traits that only define static and non-static methods
  • Safely serialize/deserialize with serialize() and igbinary_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

  • Designed and intended to be compared by identity, not equality
  • Cannot be extended or abstract (no inheritance, always final)
  • Cannot be cloned
  • Cannot declare static or object properties (or PHP 8.4 property hooks)
  • Cannot declare __construct() or __destruct() methods
  • Cannot implement magic methods except __call(), __callStatic(), and __invoke()
  • Stateless (almost...)
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;
    }
}

Statelessness Doesn't Apply to Method Scope

wkdb.yt/enums

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;
}
wkdb.yt/enums

Inheritance is Prohibited

Use Unit Types to Combine Enumerations

public function getLight(): TrafficLight|WeirdLight;

Alternatively, both enums can implement a dummy interface

interface Lights extends \BackedEnum
{
	// dummy interface
}

public function getLight(): Lights

ENUMS Can Have Constants

enum HttpMethod: string
{
    public const string GET = 'GET';
    public const string POST = 'POST';
    public const string PUT = 'PUT';
    public const string PATCH = 'PATCH';
    public const string DELETE = 'DELETE';
    public const string OPTIONS = 'OPTIONS';
    public const string HEAD = 'HEAD';
    public const string TRACE = 'TRACE';
    public const string CONNECT = 'CONNECT';

    case Get = self::GET;
    case Post = self::POST;
    case Put = self::PUT;
    case Patch = self::PATCH;
    case Delete = self::DELETE;
    case Options = self::OPTIONS;
    case Head = self::HEAD;
    case Trace = self::TRACE;
    case Connect = self::CONNECT;
}
wkdb.yt/enums

Enums Are Not Stringable

wkdb.yt/enums

...and that's ok - enums aren't strings.

"Stringable" !== "Serializable"

Enums Are Not Stringable

final class SomeSubscriber
{
	public static function getSubscribedEvents()
	{
		return ['http_method' => 'GET'];
	}
}
wkdb.yt/enums
final class SomeSubscriber
{
	public static function getSubscribedEvents()
	{
		return ['http_method' => HttpMethod::GET->value];
	}
}

Enums Are Not Stringable

return RectorConfig::configure()
    ->withConfiguredRule(StringToBackedEnumValueRector::class, \array_map(
        StringToBackedEnumValue::make(...),
        HttpMethod::cases(),
    ));
wkdb.yt/enums

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

The Simplest PHP ENUM

enum Caseless
{
}

var_dump(Caseless::cases()); // []
wkdb.yt/enums

This is perfectly legal...

...but probably useless

The Next Simplest PHP ENUM: 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 MissingOrEmpty;
    case Overflow;
    case Underflow;
    case InvalidValue;
}

function validate(mixed $value): string|ValidationError
{
    // 
}
wkdb.yt/enums

Enums for Error Handling

enum ValidationError
{
    case MissingOrEmpty;
    case Overflow;
    case Underflow;
    case InvalidValue;
}

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 MissingOrEmpty;
    case Overflow;
    case Underflow;
    case InvalidValue;

    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

Enums Can Use Traits

wkdb.yt/enums
/**
 * @phpstan-require-implements \BackedEnum
 */
trait HasBackedEnumFunctionality
{
    public static function instance(int|string|self $value): static
    {
        return $value instanceof static ? $value : self::from($value);
    }

    public static function values(): array
    {
        return \array_column(self::cases(), 'value', 'name');
    }
}

Enums Can Use Traits

wkdb.yt/enums
CardSuit::instance('♣') === CardSuit::Clubs;
CardSuit::instance(CardSuit::Diamonds) === CardSuit::Diamonds;

CardSuit::values() === [
    'CLUBS' => '♣',
    'DIAMONDS' => '♦',
    'HEARTS' => '♥',
    'SPADES' => '♠',
];

What About PHP 8.4?

wkdb.yt/enums

No Significant Changes

Questions?

wkdb.yt/enums

Slides/Code/Resources

@andysnell@phpc.social

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

  • 116