Things that Exist in Known, Limited, and Discrete States
@andrewsnell
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 |
@andrewsnell
A payment result will have only one of three values:
APPROVED
DECLINED
ERROR
@andrewsnell
public function processPayment()
{
$result = $this->payment_gateway->run();
if ($result) {
if ($result->approval_code) {
return 'approved';
}
return 'declined';
}
return 'error';
}
PaymentProcessor
@andrewsnell
public function setResult($result)
{
$result = strtolower($result);
if (!in_array($result, [
'approved',
'declined',
'error',
])) {
throw new UnexpectedValueException('Not Valid!');
}
$this->result = $result;
}
PaymentModel
@andrewsnell
public function processPayment()
{
$result = $this->gateway->run();
if ($result) {
if ($result->approval_code) {
return 1;
}
return 2;
}
return 0;
}
PaymentProcessor
@andrewsnell
class PaymentResult
{
public const APPROVED = 'approved';
public const DECLINED = 'declined';
public const ERROR = 'error';
}
PaymentResult
@andrewsnell
public function processPayment()
{
$result = $this->gateway->run();
if ($result) {
if ($result->approval_code) {
return PaymentResult::APPROVED;
}
return PaymentResult::DECLINED;
}
return PaymentResult::ERROR;
}
PaymentProcessor
@andrewsnell
public function setResult($result)
{
$result = strtolower($result);
if (!in_array($result, [
PaymentResult::APPROVED,
PaymentResult::DECLINED,
PaymentResult::ERROR,
])) {
throw new UnexpectedValueException('Not Valid!');
}
$this->result = $result;
}
PaymentModel
@andrewsnell
public function setResult(string $result): void
{
$result = strtolower($result);
if (! PaymentResult::isValid($result)) {
throw new UnexpectedValueException('Not Valid!');
}
$this->result = $result;
}
PaymentModel
@andrewsnell
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, true);
}
}
PaymentResult
@andrewsnell
Class Constants help with code comprehension and centralization.
They are just syntactic sugar around scalars.
@andrewsnell
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...
@andrewsnell
Scalar Type Definitions reduce type errors.
They do not improve readability.
Working directly with scalars allows illogical operations.
@andrewsnell
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;
This is perfectly legal...
@andrewsnell
Validation reduces runtime errors.
@andrewsnell
Only applies to the current context.
Cannot trust a scalar value to not change.
function tomorrow(string &$weekday): int
{
if (WeekDay::isValid($weekday)) {
$tomorrow = ++$weekday;
if($weekday > 6){
$tomorrow = 0;
}
return $tomorrow;
}
throw new \InvalidArgumentException();
}
$today = WeekDay::SUNDAY; // (int)6
$tomorrow = tomorrow($today); // (int) 0
var_dump($today); // (int)7
A bit contrived, but we've all seen code like this...
@andrewsnell
@andrewsnell
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 |
@andrewsnell
An enumerated type is a named, language-level, wrapper 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.
@andrewsnell
@andrewsnell
enum boolean {
false = 0,
true = 1
};
@andrewsnell
@andrewsnell
#include <stdio.h>
enum payment_result {
APPROVED, // 0
DECLINED, // 1
ERROR // 2
};
int main()
{
enum payment_result result;
result = ERROR;
printf("Result: %d", result); // "Result 2"
return 0;
}
@andrewsnell
<?hh
enum PaymentResult: string {
APPROVED = 'approved';
DECLINED = 'declined';
ERROR = 'error';
};
echo PaymentResult::APPROVED . PHP_EOL; // "approved\n"
@andrewsnell
from enum import Enum
class PaymentStatus(Enum):
APPROVED = "approved"
DECLINED = "declined"
ERROR = "error"
def foo(self):
return "Hello World"
@property
def is_failed(self):
return self != self.APPROVED
print(PaymentStatus.APPROVED.value) # "approved"
print(PaymentStatus.APPROVED.is_failed) # "False"
print(PaymentStatus.DECLINED.is_failed) # "True"
print(PaymentStatus.APPROVED.foo()) # "Hello, World"
@andrewsnell
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"
}
}
@andrewsnell
@andrewsnell
@andrewsnell
PHP is a developed language, not designed!
@andrewsnell
@andrewsnell
Enumerated Types Need Strongly Typed Contexts
@andrewsnell
@andrewsnell
function setResult(PaymentResult $result): PaymentResult
{
return $result;
}
$result = PaymentResult::APPROVED();
setResult($result)
@andrewsnell
@andrewsnell
@andrewsnell
@andrewsnell
@andrewsnell
PaymentResult::APPROVED() == PaymentResult::APPROVED();
'approved' === PaymentResult::APPROVED()->value();
@andrewsnell
'approved' === (string) PaymentResult::APPROVED();
PaymentResult::make('approved') == PaymentResult::APPROVED();
@andrewsnell
"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
@andrewsnell
@andrewsnell
<?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...
@andrewsnell
abstract class Enum
{
protected $value; // The underlying scalar value
public static function __callStatic($enum, $args): self
{
return new static($enum);
}
protected function __construct(string $enum)
{
$enum = strtoupper($enum);
if (!array_key_exists($enum, static::$values)) {
throw new UnexpectedValueException();
}
$this->value = static::$values[$enum];
}
public function __set($name, $value): void
{
throw new \LogicException("Read Only");
}
Define a Self-Valid, Immutable Parent Class...
@andrewsnell
<?php
$result = PaymentResult::UNDEFINED(); // Throws Exception
$result = PaymentResult::APPROVED(); // PaymentResult
$result = PaymentResult::DECLINED(); // PaymentResult
$result = PaymentResult::ERROR(); // PaymentResult
$result->foo = "hello, world"; // Throws Exception
Using the Enum: Instantiation
@andrewsnell
// 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 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;
}
Let's Make It Castable...
@andrewsnell
<?php
$result = PaymentResult::make('approved'); // Payment Result
'approved' === (string) $result; // true
'approved' === $result->value(); // true
Using the Enum: Castable
@andrewsnell
abstract class Enum
{
// ...
public function is($value): bool
{
if ($value instanceof static) {
return $this->value === $value->value;
}
return $this->value === $value;
}
}
Let's Make It Comparable...
@andrewsnell
// COMPARABLE
PaymentResult::APPROVED() == PaymentResult::APPROVED(); // true;
PaymentResult::APPROVED() === PaymentResult::APPROVED(); // false;
PaymentResult::APPROVED() == PaymentResult::DECLINED(); // false
$result = PaymentResult::APPROVED(); // PaymentResult
$result->is('approved'); // true
$result->is('declined'); // false;
$result->is(PaymentResult::APPROVED()); // true
$result->is(PaymentResult::DECLINED()); // false
Using the Enum: Comparison
@andrewsnell
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();
}
@andrewsnell
<?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',
];
protected static $cache = [];
}
What about caching the object?
@andrewsnell
abstract class Enum
{
protected $value;
public static function __callStatic($enum, $args): self
{
return static::$cache[$enum] ??= new static($enum);
}
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);
}
// ...
What about caching the object?
@andrewsnell
Caching !== Comparison by Identity
@andrewsnell
$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"
}
spatie/enum
myclabs/php-enum
BenSampo/laravel-enum
@andrewsnell
/**
* @method static self approved()
* @method static self declined()
* @method static self error()
*/
class PaymentResult extends Enum
{
}
$result = PaymentResult::approved();
$result = PaymentResult::make('approved');
$result->getValue(); // 'approved';
$result->isApproved(); // true;
$result->isEqual(PaymentResult::approved()); // true
$result->isEqual('approved'); // true
$result->isEqual(PaymentResult::declined()); // false
@andrewsnell
spatie/enum Package
<?php
declare(strict_types=1);
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()
* @method static KpiReportPeriod CUSTOM()
*/
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';
private const CUSTOM = 'custom';
}
@andrewsnell
MyCLabs\Enum Package
final class UserPermissions extends FlaggedEnum
{
private const READ_COMMENTS = 1 << 0; // 1
private const WRITE_COMMENTS = 1 << 1; // 2
private const EDIT_COMMENTS = 1 << 2; // 4
private const DELETE_COMMENTS = 1 << 3; // 8
// Shortcuts
public const Member = self::READ_COMMENTS | self::WriteComments;
public const Moderator = self::Member | self::EditComments;
public const Admin = self::Moderator | self::DeleteComments;
}
$permissions = new UserPermissions([
UserPermissions::READ_COMMENTS(),
UserPermissions::WRITE_COMMENTS(),
UserPermissions::EDIT_COMMENTS(),
]);
$permissions->hasFlag(UserPermissions::READ_COMMENTS()); // true
$permissions->hasFlag(UserPermissions::DELETE_COMMENTS()); // false
@andrewsnell
BenSampo/laravel-enum Package
Oversimplified!
@andrewsnell
@andrewsnell
@andrewsnell
final class AccountStatus extends FiniteStateMachineEnum
{
protected static $cache = [];
protected static $values = [
'ACCEPTED' => 'accepted',
'APPROVED' => 'approved',
'DECLINED' => 'declined',
'ACTIVATING' => 'activating',
'ACTIVATING_FAILED' => 'activating_failed',
'ACTIVE' => 'active',
'DEACTIVATING' => 'deactivating',
'DEACTIVATED' => 'deactivated',
];
protected static $transitions = [
'accepted' => ['APPROVED', 'DECLINED'],
'approved' => ['ACTIVATING'],
'declined' => [],
'activating' => ['ACTIVATING_FAILED', 'ACTIVE'],
'activating_failed' => ['ACTIVATING', 'DECLINED'],
'active' => ['DEACTIVATING'],
'deactivating' => ['ACTIVE', 'DEACTIVATED'],
'deactivated' => ['ACTIVATING'],
];
@andrewsnell
abstract class FiniteStateMachineEnum extends Enum
{
public function canTransitionTo(self $enum): bool
{
return in_array($enum->value, static::$transitions[$this->value], true);
}
public function transition(self $enum): self
{
if (!$enum instanceof static) {
throw new RuntimeException('Invalid Enum');
}
if (!$this->canTransitionTo($enum)) {
throw new RuntimeException('Invalid Transition');
}
return $enum;
}
@andrewsnell
$status = AccountStatus::ACTIVATING(); // AccountStatus
$status->canTransitionTo(AccountStatus::DECLINED()); // false
$status->canTransitionTo(AccountStatus::ACTIVE()); // true
$status->transition(AccountStatus::DECLINED()); // throws exception
$new_status = $status->transition(AccountStatus::ACTIVE()); // AccountStatus
$status == AccountStatus::ACTIVATING(); // true (immutable)
$new_status == AccountStatus::ACTIVE(); // true
@andrewsnell
@andrewsnell
Slides & Resources
https://bit.ly/30StLbh