PHP 8.1: Enums

Version8.1
TypeNew Feature

PHP 8.1 adds support for Enumerations. An Enumeration, or an Enum for short, is an enumerated type that has a fixed number of possible values.

RFC in progress.
This RFC is still being ironed out with some implementation details. This post will be actively updated, and this noticed removed once the Enum feature is merged to PHP.

A popular analogy for an Enum is suits in a deck of playing cards. A deck of playing cards has four suits, and they are fixed: Clubs, Diamonds, Hearts, and Spades.

In PHP, these suits can be enumerated with an Enum:

enum Suit {
    case Clubs;
    case Diamonds;
    case Hearts;
    case Spades;
}

With Suit Enum, it is now possible to enforce types when accepting or returning a suit value:

function pick_card(Suit $suit) {}
pick_card(Suit::Clubs);
pick_card(Suit::Diamonds);
pick_card(Suit::Hearts);
pick_card(Suit::Spades);

In contrast to using special strings or numbers internally (i.e. magic numbers) to store and work with parameters, Enums make the application code more readability, and avoids unexpected application state.

Enum Syntax

PHP 8.1 reserves and uses enum keyword to declare Enums. The syntax is similar to a trait/class/interface syntax:

enum Suit {
    case Clubs;
    case Diamonds;
    case Hearts;
    case Spades;
}

Enums are declared with the enum keyword, followed by the name of the Enum. An Enum can optionally declare string or int as backed values. Enums can also extend a class and/or implement interfaces.

Internally at the PHP parser level, there is a new token with named T_ENUM with value 369 assigned.

Enums can also hold a value for each case, which makes them Backed Enums.

enum HTTPMethods: string {
    case GET = 'get';
    case POST = 'post';
}

Following is an example of an Enum that declares backed value type, and implements an interface.

enum RolesClassLikeNamespacedEnum: string implements TestFor {  
  case Admin = 'Administrator';  
  case Guest = 'Guest';  
  case Moderator = 'Moderator';  
}

New enum_exists function

PHP 8.1 also adds a new enum_exists function to check if a given Enum exists.

function enum_exists(string $enum, bool $autoload = true): bool {}

Note that due to class-semantics of Enums, class_exists function also returns true for an Enum.

UnitEnum interface

Enums that are not backed with a value automatically implement UnitEnum interface.

interface UnitEnum {
    public static function cases(): array;
}   

Enums cannot explicitly implement this interface as it is internally done by the engine. This is only to assist in determining the type of a given Enum. The UnitEnum::cases method returns an array of all cases of a given Enum.

PHP classes are not allowed to implement this interface, and results in an error if attempted:

class FakeEnum implements UnitEnum {}
Fatal error: Non-enum class FakeEnum cannot implement interface UnitEnum in ... on line ...

Although Enums allow methods declared on them, declaring a method with name cases is not allowed:

enum Foo {
    public function cases(): array{}
}
Fatal error: Cannot redeclare Foo::cases() in ... on line ...

BackedEnum interface

If an Enum declares scalar backed values, that Enum automatically an interface called BackedEnum. Similar to UnitEnum interface, it is not possible to explicitly implement BackedEnum interface.

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

See BackedEnum::from and BackedEnum::tryFrom for their usage information.

Standard classes are not allowed to implement this interface.

class FakeEnum implements BackedEnum {}
Fatal error: Non-enum class FakeEnum cannot implement interface BackedEnum in ... on line ...

Similar to the restriction on not allowing a method with name cases is not allowed, any backed Enum must not declare a from or tryFrom method:

enum Foo: int {
    public function from() {}
    public function tryFrom() {}
}
Fatal error: Cannot redeclare Foo::from() in ... on line ...
Fatal error: Cannot redeclare Foo::tryFrom() in ... on line ...

Declaring Enums

Enums are internally implemented on top PHP classes, and they inherit most of the class semantics with additional restrictions imposed.

Enums support namespaces, autoloading, they can have methods (but not properties), implementing interfaces, and many other behaviors associated with PHP classes.

A basic enum is simply an enum structure, where each case is declared with the case keyword. With Enums supported in PHP 8.1, PHP now reserves "enum" as a reserved word, and prevents any functions, classes, interfaces, etc. from being created with enum. It can be part of a namespace due to changes in how PHP considers reserved keywords in namespaced values.

Enums can have zero or more cases

Within an enum structure, it may contain any number of "cases", from zero to unlimited. Both of these Enum declarations are valid:

enum ErrorStates {
}
enum HTTPMethods {
    case GET;
    case POST;
}

Enums may have optional values

It is possible to assign a string or int value to each case in an Enum. This can be useful when serializing data, storing them in a database, etc.

Enums that hold a value, i.e. a backed Enum, must:

  1. Declare the scalar type in the Enum declaration. Only string or int is allowed.
  2. Assign values for all cases.
  3. Hold values of same scalar type. It is not allowed to store string and int values mixed.
  4. Cases and assigned values are unique
enum Suit: string {
    case Clubs = '♣';
    case Diamonds = '♦';
    case Hearts = '♥';
    case Spades = '♠';
}

Backed Enums must declare the scalar type

In order for an Enum to be to associate values for each case (i.e. a Backed Enum), it must declare the scalar type at the Enum declaration.

Not doing so results in an error:

enum HTTPMethods {
    case GET = 'get';
    case POST = 'post';
}
Fatal error: Case GET of non-scalar enum HTTPMethods must not have a value in ... on line ...

Enums only support string and int scalar types. Any other type, including bool, null, ?string, ?int, or even string|int union types are not allowed.

enum HTTPMethods: object {
    case GET = 'get';
    case POST = 'post';
}
Fatal error: Enum scalar type must be int or string, object given in ... on line ...

Backed Enums must assign values for all cases

This section is work-in-progress

If an Enum declares the scalar type of values, it must set a value for all cases:

enum HTTPMethods: string {
    case GET;
    case POST;
}

In the snippet above, HTTPMethods Enum is declared to contain string, but the cases are not assigned a value. This is not allowed, and results in an error.

Backed Enums must hold values of same scalar type

With a Backed Enum, the values assigned to each case must be of the same type as declared in the type.

PHP strictly enforces this even with strict_types not enabled.

enum HTTPMethods: int {
    case GET = 1;
    case POST = '2';
}
Fatal error: Enum case type string does not match enum scalar type int in ... on line ...

Backed Enum cases and values must be unique

This section is work-in-progress

A valid Enum must not contain duplicate cases, or duplicate values. For example, both of these declarations are invalid:

enum Test {
    case FOO;
    case FOO;
}
enum Test: string {
    case FOO = 'baz';
    case BAR = 'baz';
}

Enum values must be unique as well, because BackedEnum::from supports retrieving an Enum object from a given scalar value.

Case-Sensitivity

The name of the Enum itself is case-insensitive, and it follows how PHP treats classes and functions in a case-insensitive manner.

Individual cases in an Enum are case-sensitive.

enum CaseInSenSitive {
    case bAR;
    case Bar;
}

Class Semantics in Enums

Namespaces

Enums support namespaces. They follow the standard namespace syntax that is otherwise used in classes, traits, interfaces, functions, etc.

namespace Foo\Bar;

enum HTTPMethods {}

Autoloading

Just like PHP supports autoloading for classes, traits, and interfaces, Enums support autoloading as well.

Note that this might require updates to class-map generators that scan files for autoloadable items.

Magic constants

Enums fully support all magic constants that PHP supports for classes.

  • ::class constant that refers to the name of the Enum itself.
  • __CLASS__ magic constant that refers to the name of the Enum from within the Enum.
  • __FUNCTION__ in Enum method context.
  • __METHOD__ in Enum method context.

Class/object functions and instanceof

Enums behave similar to classes when they are used with functions that support inspecting classes and objects.

For example, gettype, is_a, is_object, get_class and get_debug_type (new in PHP 8.0) functions behave as if an Enum is a standard PHP object.

enum Suit {
    case Clubs;
    case Diamonds;
    case Hearts;
    case Spades;
}

gettype(Suit::Clubs); // "object"

is_object(Suit::Spades); // true
is_a(Suit::Clubs, Suit::class); // true

get_class(Suit::Clubs); // "Suit"
get_debug_type(Suit::Clubs); // "Suit"

Suit::Clubs instanceof Suit; // true
Suit::Clubs instanceof UnitEnum; // true
Suit::Clubs instanceof object; // false

Enums with Properties, Methods, and Traits

Enums are made in a way that it can compare one Enum case with another. Enums must be stateless, as in they do not allow storing properties in them.

Enums allow methods

Enums can contain methods. They also support standard method visibility modifiers as well as static methods.

This can be quite useful use cases such as declaring a label(): string method that returns a user-friendly label.

enum HTTPStatus: int {
    case OK = 200;
    case ACCESS_DENIED = 403;
    case NOT_FOUND = 404;

    public function label(): string {
        return static::getLabel($this);
    }

    public static function getLabel(self $value): string {
        return match ($value) {
            HTTPStatus::OK => 'OK',
            HTTPStatus::ACCESS_DENIED => 'Access Denied',
            HTTPStatus::NOT_FOUND => 'Page Not Found',
        };
    }
}

echo HTTPStatus::ACCESS_DENIED->label(); // "Access Denied"
echo HTTPStatus::getLabel(HTTPStatus::ACCESS_DENIED); // "Access Denied"

The snippet above uses match expressions added in PHP 8.0

Enums can implement interfaces

Enums can implement interfaces. Enums must fulfill the contracts of the interfaces just like standard class must.

interface Foo {}
enum FooEnum implements Foo {}

Enums must not contain properties

One of the most important differences between an Enum and a class is that Enums are not allowed to have any state. Declaring properties, or setting properties is not allowed. static properties are not allowed either.

enum Foo {
    private string $test;
    private static string $test2;
}
Fatal error: Enums may not include member variables in ... on line ...

Further, dynamically setting properties is not allowed either:

enum Foo {
    case Bar;
}
$bar = Foo::Bar;
$bar->test = 42;
Error: Enum properties are immutable in ...:...

Instantiating with new is not allowed

Although Enum cases themselves are objects, it is not allowed to instantiate them with the new construct.

Both of the following new constructs are not allowed:

enum Foo {
    case Bar;
}

new Foo(); // Fatal error: Uncaught Error: Cannot instantiate enum Foo
new Foo::Bar(); Parse error: syntax error, unexpected identifier "Bar", expecting variable or "$"

Enums cannot be extended, and must not inherit

This section is work-in-progress

Enums are internally declared as final, and Enum may not inherit from another Enum or a class.

enum Foo extends Bar {}
Parse error: syntax error, unexpected token "extends", expecting "{" in ... on line ...

If a class attempts to extend an Enum, that will result in an error as well because all Enums are declared final.

enum Foo {}
class Bar extends Foo {}
Fatal error: Class Bar may not inherit from final class (Foo) in ... on line ...

Enums support property-less traits

Enums can use traits, as long as the trait does not declare any properties.

trait NamedDocumentStatus {
    public function getStatusName(): string {}
}

enum DocumentStats {
    use NamedDocumentStatus;

    case DRAFT;
    case PUBLISHED;
}

If the traits used contains any properties (static or otherwise), using that trait results in a fatal error:

trait NotGood {
    public string $value;
}
enum Foo {
    use NotGood;
}
Fatal error: Enum "Foo" may not include properties in ... on line ...

Disallowed magic methods

To prevent Enum objects from having any state, and to ensure two Enums are comparable, Enums disallow implementing several magic methods:

  • __get(): To prevent maintaining state in Enum objects.
  • __set(): To prevent dynamic property assignment and maintaining state.
  • __construct(): Enums do not support new Foo() construct all.
  • __destruct(): Enums must not maintain state.
  • __clone(): Enums are uncloneable objects.
  • __sleep(): Enums do not support life-cycle methods.
  • __wakeup(): Enums do not support life-cycle methods.
  • __set_state(): To prevent coercing state to Enum objects.

All of the following magic method declarations are not allowed.

enum Foo {
    public function __get() {}
    public function __set() {}
    public function __construct() {}
    public function __destruct() {}
    public function __clone() {}
    public function __sleep() {}
    public function __wakeup() {}
    public function __set_state() {}
}

If they are declared, it will trigger a fatal error with a message similar to this:

Fatal error: Enum may not include __get in ... on line ...

Classes vs Enums

Enums store fixed values, with optional backed values, and they do not allow state. They serve distinctly different purposes, but Enums share some semantics with classes.

Classes Enums
Syntax class Foo {} enum Foo {}
Properties
Static Properties
Methods
Static Methods
Autoloading
Instantiating: new Foo()
Implement interfaces
Inheritance: Foo extends Bar
Magic Constants: ::class, __CLASS__, etc.
Object Comparison Foo === Foo Not equal Equals
Traits Supports Supports without properties

Using Enums

The main use case of Enums is type safety. The PHP engine will ensure that a passed or returned value is belongs to one of the allowed values. Without having to validate passed values belongs to one of the allowed types, the PHP engine enforces it, and allows IDEs and static analyzers to highlight potential bugs as well.

With Backed Enums, it is possible to store the enumerated values in a database or other storage, and restore a programmatically identical object.

Comparing Enum Values

At any given time, two Enums holding the same value is considered identical, just as how PHP considers two strings to be identical. This is a major difference between two objects, because two objects from an instantiated class are not considered identical even though they hold exact same values.

new stdClass() === new stdClass(); // false
Suit::Hearts === Suit::Hearts; // true

Enums as Parameter, Property and Return Types

Enums can be used as parameter, return, and property types. When the Enum name is used, the passed/set/return values must be one of the Enumerated values.

enum Suit {
    case Clubs;
    case Diamonds;
    case Hearts;
    case Spades;
}
function play(Suit $suit, string $value) {}

The play function accepts an enumerated value of Suit, and passing any other value will result in a \TypeError. This can greatly improve the code because PHP validates the passed values without having to write additional validations inside the play function.

play(Suit::Clubs, 'J');
play(Suit::Hearts, 'Q');

If any value other than a Suit Enum value is passed, it will cause a type error:

play('clubs', 'J');
TypeError: play(): Argument #1 ($suit) must be of type Suit, string given

It does not allow to use values from another Enum either:

play(UnoColors::Blue, 'J');
TypeError: play(): Argument #1 ($suit) must be of type Suit, UnoColors given

Further, if an undefined Enum value is used, it will cause an undefined class constant because Enum values and class constants share the same syntax.

Enum name and value properties

Each Enum contains a name property that refers to the name of the property. This value is read-only.

enum Suit {
    case Clubs;
    case Diamonds;
    case Hearts;
    case Spades;
}

echo Suit::Clubs->name; // "Clubs"

In a Backed Enum, there is also value property for the backed value (either string or int).

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

echo Suit::Clubs->name; // "Clubs"
echo Suit::Clubs->value; // "♣"

Attempting to change name or value properties will result in an error:

Suit::Clubs->name = 'Hearts';
Error: Enum properties are immutable in ...:...

Further, Enums without a backed value (UnitEnum) do not have a value property. Only Backed Enums (BackedEnum) have value property. Attempting to use an undefined property (including value) currently raises a warning in PHP 8.1.

enum Suit {
    case Clubs;
    case Diamonds;
    case Hearts;
    case Spades;
}

echo Suit::Clubs->value;
Warning: Undefined property: Suit::$value in ... on line ...

Getting All Enum Values

Both UnitEnum and BackedEnum Enums support a ::cases method, that returns all values of the Enum.

enum Suit {
    case Clubs;
    case Diamonds;
    case Hearts;
    case Spades;
}

Suit::cases();
// [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spaces]

The Suit::cases() method returns an array of all possible Enums. The return values are Enum objects themselves, and not the name or value properties.

Getting Enum by a backed value

All Backed Enums (BackedEnum) support from and tryFrom methods that allow retrieving an instance from the backed value.

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

$clubs = Suit::from('♥');

var_dump($clubs); // enum(Suit::Hearts)
echo $clubs->name; // "Hearts";
echo $clubs->value; // "♥"

If there is no Enum by that value, PHP will throw a ValueError exception.


There is also a tryFrom method, that returns null if an Enum does not exist by that value.

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

$clubs = Suit::tryFrom('not-existing');

var_dump($clubs); // null

Serializing/Unserializing Enums

Both Backed and Unit Enums can be serialized and unserialized using built-in serialize and unserialize functions.

The serialized form will have the identifier E, and the name of the Enum.

enum PostStatus {
    case DRAFT;
}
serialize(PostStatus::DRAFT);
E:16:"PostStatus:DRAFT";

On Backed Enums, the serialized values continue be the name of the member; not the backed value:

enum FooEnum: string {
    case Foo = 'bartest';
}
serialize(FooEnum::Foo)
E:11:"FooEnum:Foo";

Note that the unserialization syntax is not compatible with older PHP versions. Any serialized strings of Enums cannot be unserialized on older PHP versions. Attempting to doing so results in a warning (PHP Notice: unserialize(): Error at offset ...), and the unserialize returning false.


Backwards Compatibility Impact

Enums use a new syntax, and it is not possible to use Enums in PHP versions prior to 8.1.

Native PHP approaches such as myclabs/php-enum are not compatible with PHP 8.1 Enums.

The Enum syntax will result in a ParserError in PHP < 8.1. Further, it is not possible to unserialize a serialized Enum in older PHP versions.


RFC Discussion Implementation