enums​

PHP's Missing Data Type

Andy Snell | @andrewsnell

php[world] 2019

What Does It Mean To Be Enumerable?

Things that Exist in Known, Limited, and Discrete States

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
american_express

ProcessING a Credit Card Payment

A processed payment can have one of three values

APPROVED

DECLINED

ERROR

public function processPayment()
{
    $result = $this->gateway->run();

    if ($result) {
        if ($result->approval_code) {
            return 'approved';
        }
        
        return 'declined';
    }
    
    return 'error';
}
public function setResult($result)
{
    $result = strtolower($result);
    
    if (!in_array($result, ['approved', 'declined', 'error'])) {
        throw new UnexpectedValueException('Not A Valid Result');
    }

    $this->result = $result;
}
PaymentProcessor
PaymentModel

PROCESSING A CREDIT CARD PAYMENT

Start with "Magic Strings"

public function processPayment()
{
    $result = $this->gateway->run();

    if ($result) {
        if ($result->approval_code) {
            return 1;
        }
        
        return 2;
    }
    
    return 0;
}
public function setResult($result)
{    
    if (!in_array($result, [0, 1, 2], true)) {
        throw new UnexpectedValueException('Not A Valid Result');
    }

    $this->result = $result;
}
PaymentProcessor
PaymentModel

PROCESSING A CREDIT CARD PAYMENT

Or with "Magic Integers"

class PaymentResult
{
    public const APPROVED = 'approved';
    public const DECLINED = 'declined';
    public const ERROR = 'error';
}
public function setResult($result)
{
    $result = strtolower($result);

    if (!in_array($result, [PaymentResult::APPROVED, PaymentResult::DECLINED, PaymentResult::ERROR])) {
        throw new UnexpectedValueException('Not A Valid Result');
    }

    $this->result = $result;
}
PaymentResult
PaymentModel

PROCESSING A CREDIT CARD PAYMENT

public function processPayment()
{
    $result = $this->gateway->run();

    if ($result) {
        if ($result->approval_code) {
            return PaymentResult::APPROVED;
        }

        return PaymentResult::DECLINED;
    }

    return PaymentResult::ERROR;
}
PaymentProcessor

Using Class Constants

class PaymentResult
{
    public const APPROVED = 'approved';
    public const DECLINED = 'declined';
    public const ERROR = 'error';
}
public function setResult(string $result): void
{
    $result = strtolower($result);

    if (!in_array($result, [PaymentResult::APPROVED, PaymentResult::DECLINED, PaymentResult::ERROR])) {
        throw new UnexpectedValueException('Not A Valid Result');
    }

    $this->result = $result;
}
PaymentResult
PaymentModel

PROCESSING A CREDIT CARD PAYMENT

public function processPayment(): string
{
    $result = $this->gateway->run();

    if ($result) {
        if ($result->approval_code) {
            return PaymentResult::APPROVED;
        }

        return PaymentResult::DECLINED;
    }

    return PaymentResult::ERROR;
}
PaymentProcessor

With Scalar Type Declarations

class PaymentResult
{
    public const APPROVED = 'approved';
    public const DECLINED = 'declined';
    public const ERROR = 'error';
    
    public static function isValid(string $value): bool
    {
    	$class = get_called_class();
        $reflect = new ReflectionClass($class);
        $constants = $reflect->getConstants();
        return in_array($value, $constants);
    }
}
public function setResult(string $result)
{
    $result = strtolower($result);

    if (! PaymentResult::isValid($result)) {
        throw new UnexpectedValueException('Not A Valid Result');
    }

    $this->result = $result;
}
PaymentResult
PaymentModel

PROCESSING A CREDIT CARD PAYMENT

public function processPayment(): string
{
    $result = $this->gateway->run();

    if ($result) {
        if ($result->approval_code) {
            return PaymentResult::APPROVED;
        }

        return PaymentResult::DECLINED;
    }

    return PaymentResult::ERROR;
}
PaymentProcessor

With Better Validation

What's Wrong WitH Things Now?

  • Class Constants

    • Help with readability, and to centralize truth

    • Still just syntactic sugar around a scalar value

What's Wrong WitH Things Now?

class PaymentResult
{
    public const APPROVED = 'approved';
    public const DECLINED = 'declined';
    public const ERROR = 'error';
}
PaymentResult
class AccountStatus
{
    public const APPROVED = 'approved';
    public const ACTIVATING = 'activating';
    public const DEACTIVATING = 'deactivating';
    public const DEACTIVATED = 'deactivated';
    public const DECLINED = 'declined';
    public const ERROR = 'error';
}
AccountStatus
$result = $this->gateway->run($transaction);

if($result->status === 201){
	$transaction->setResult(AccountStatus::APPROVED)
}

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

What's Wrong WitH Things Now?

 

 

  • Scalar Type Declarations

    • ​Can reduce some type related errors
    • Do not add much in the way of readability
    • Allow for illogical operations, e.g. adding magic numbers

What's Wrong WitH Things Now?

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;

What's Wrong WitH Things Now?

 

  • Validation

    • Validation logic can be centralized
    • We still have to call validate everywhere

We don't want to pass around a string.

We Want To Pass A "Payment Status"

We Want AN Enumerated Type

In Summary

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?

What is an Enumerated Type?

Enumerated Type Defined

An enumerated type restricts the possible values of an instance to a discrete set of named members.

In practice, this means that an enum variable can have any value in the set and only values in the set can be values.

What is an Enumerated Type?

The Enumerated Type PHP Does Have...

enum boolean {
    false = 0,
    true = 1
};

Enumerated Types In Other Languages

Python

from enum import Enum
class PaymentResult(Enum):
	APPROVED = "approved"
	DECLINED = "declined"
	ERROR = "error"
    
PaymentResult.APPROVED.value == "approved"
class Main {

  public enum PaymentResult {
      APPROVED   ("approved"),
      DECLINED ("declined"),
      ERROR  ("error");

      private final String value;
      PaymentResult(String value) {
          this.value = value;
      }

      public final String value(){
        return this.value;
      }
  }

  public static void main(String[] args) {
    PaymentResult result = PaymentResult.DECLINED;
    System.out.println(result.value()); // "declined"
  }
}

Java

Hack

enum PaymentResult: string {
    Approved = 'approved';
    Declined = 'declined';
    Error = 'error';
};

Why Doesn't PHP Have Enums?

So, Why Doesn't PHP Have Enums?

The Language Was Not Ready for It

So, Why Doesn't PHP Have Enums?

Picking Implementation Details is Hard

So, Why Doesn't PHP Have Enums?

SplEnum

So, Why Doesn't PHP Have Enums?

Workable Userland Solutions Exist

Enumerated Object

function seeResult(PaymentResult $result): PaymentResult
{
    return $result;
}


$result = PaymentResult::APPROVED();
setResult($result)

What Should An Enumerated Object Look Like?

Properties of an Enumerated Object

Immutable

Properties of an Enumerated Object

Comparable

PaymentResult::APPROVED() == PaymentResult::APPROVED();

'approved' === PaymentResult::APPROVED()->value();

Properties of an Enumerated Object

Castable

'approved' === (string) PaymentResult::APPROVED();

PaymentResult::make('approved') == PaymentResult::APPROVED();

Different approaches; same goal

Value Object

Registry of Singletons

Building Our Enum

Live Code Demo

Don't Reinvent the Wheel

spatie/enum
myclabs/php-enum
dbalabka/php-enumeration
BenSampo/laravel-enum

Now That We Have OnE,

What Else Can We Do with It?

Finite State Machine

Oversimplified!

Finite State Machine

  • A finite-state machine is a mathematical model of computation.
  • It can be in exactly one of a finite number of states at any given time.
  • It can change from one state to another in response to inputs or under conditions
  • The change from one state to another is called a transition.
  • Is defined by a list of states, the initial state, and the transitions.
final class AccountStatus extends Enum
{
	
    // Required Properties for Enum Defined Here...

    protected static $transitions = [
        'accepted' => ['APPROVED', 'DECLINED'],
        'approved' => ['ACTIVATING'],
        'declined' => [],
        'activating' => ['ACTIVATING_FAILED', 'ACTIVE'],
        'activating_failed' => ['ACTIVATING', 'DECLINED'],
        'active' => ['DEACTIVATING'],
        'deactivating' => ['ACTIVE', 'DEACTIVATED'],
        'deactivated' => ['ACTIVATING'],
    ];

    /**
     * @return AccountStatus[]
     */
    public function getTransitions(): array
    {
        return array_map([__CLASS__, 'getInstance'], self::$transitions[$this->value]);
    }

    public function canTransitionTo(AccountStatus $account_status): bool
    {
        return in_array($account_status, $this->getTransitions());
    }
}

Oversimplified Finite State Machine

class AccountStatusTest extends TestCase
{
    /**
     * @test
     */
    public function getTransitions_will_return_instances_of_AccountStatus(): void
    {
        $status = AccountStatus::ACTIVATING();
        $transitions = $status->getTransitions();
        $this->assertCount(2, $transitions);
        $this->assertContains(AccountStatus::ACTIVATING_FAILED(), $transitions);
        $this->assertContains(AccountStatus::ACTIVE(), $transitions);
    }

    /**
     * @test
     * @testWith    ["ACTIVATING", "ACTIVE", true]
     *              ["ACTIVATING", "ACTIVATING_FAILED", true]
     *              ["ACTIVATING", "DECLINED", false]
     */
    public function canTransitionTo_will_return_correctly($status, $transition, $expected): void
    {
        $status = AccountStatus::$status();
        $transition = AccountStatus::$transition();
        $this->assertSame($expected, $status->canTransitionTo($transition));
    }
}

OVERSIMPLIFIED FINITE STATE MACHINE

So, What's Next?

If and when will we get an enum type in PHP?

  • Language Has Grown, Seems Ready Soon
  • Upcoming RFCs that Affect Types
  • Definite Advantages over Enumerated Objects
  • Still, May End Up Using Enumerated Objects Anyway

Thank you!

Andy SnelL

@andrewsnell

Slides & Resources

https://bit.ly/ 2JbjYFq

Enums: PHP's Missing Data Type

By Andy Snell

Enums: PHP's Missing Data Type

PHP may not have a native data type for an enumerated type (“enum”), like other programming languages, but there are userland solutions we can leverage to get access to this powerful data type. We’ll see how representing things like statuses with enums provides immutability, improved readability, and type safety — preventing the kind of errors that happen with “magic strings” and class constants. In this session, we’ll be making our own immutable enums from scratch in order to explore the concept, but we’ll also introduce two libraries for use in your production code. We’ll also demystify the imposing-sounding “finite state machine” by using using immutable enum objects to regulate the transitions between statuses.

  • 49
Loading comments...

More from Andy Snell