PHP 8.1: Enums
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.
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:
- Declare the scalar type in the Enum declaration. Only
string
orint
is allowed. - Assign values for all cases.
- Hold values of same scalar type. It is not allowed to store
string
andint
values mixed. - 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';
}
Enum backing 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 properties in ... on line ...
Further, dynamically setting properties is not allowed either:
enum Foo {
case Bar;
}
$bar = Foo::Bar;
$bar->test = 42;
Error: Error: Cannot create dynamic property Foo::$test 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 cannot extend 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 supportnew 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';
Cannot use temporary expression in write context 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.