Переліки (enum) · PHP Manual
Огляд переліків
Переліки (enum) дозволяють розробнику визначити власний тип, обмежений одним зі скінченної кількості можливих значень. Це особливо корисно під час опису доменної моделі, бо дає змогу «зробити некоректні стани непредставними».
Переліки є в багатьох мовах і мають різний набір можливостей. У PHP перелік - це особливий різновид обʼєкта. Сам перелік є класом, а всі його можливі випадки (cases) - це обʼєкти-одинаки цього класу. Отже, випадки переліку є повноцінними обʼєктами і можуть використовуватися скрізь, де допустимий обʼєкт, зокрема у перевірках типів.
Найвідоміший приклад переліку - вбудований тип boolean, перелічуваний тип із допустимими значеннями true та false. Переліки дають розробникам змогу визначати власні як завгодно складні переліки.
Базові переліки
Переліки схожі на класи й поділяють ті самі простори імен, що класи, інтерфейси та трейти. Вони так само підлягають автозавантаженню. Перелік визначає новий тип, який має фіксовану обмежену кількість допустимих значень.
<?php
enum Suit
{
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
Це оголошення створює новий перелічуваний тип Suit, який має чотири й лише чотири допустимі значення: Suit::Hearts, Suit::Diamonds, Suit::Clubs і Suit::Spades. Змінним можна присвоїти одне з цих допустимих значень. Функцію можна перевіряти за типом переліку, і тоді передати можна лише значення цього типу.
<?php
enum Suit
{
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
function pick_a_card(Suit $suit)
{
var_dump($suit);
}
$val = Suit::Diamonds;
// OK
pick_a_card($val);
// OK
pick_a_card(Suit::Clubs);
// TypeError: pick_a_card(): Argument #1 ($suit) must be of type Suit, string given
pick_a_card('Spades');
Перелік може мати нуль або більше визначень case, без верхньої межі. Перелік без жодного випадку синтаксично коректний, хоч і досить марний.
Для випадків переліку діють ті самі синтаксичні правила, що й для будь-якої мітки в PHP, див. Константи.
Типово випадки не підкріплені скалярним значенням. Тобто Suit::Hearts не дорівнює "0". Натомість кожен випадок підкріплений обʼєктом-одинаком із такою назвою. А отже:
<?php
enum Suit
{
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
$a = Suit::Spades;
$b = Suit::Spades;
if ($a === $b) {
print "Suits match using ===\n";
}
if ($a instanceof Suit) {
print "Suits match using instanceof\n";
}
if ($a !== 'Spades') {
print "Suit does not match the string\n";
}
Це також означає, що значення переліку ніколи не бувають < чи > одне за одного, бо такі порівняння для обʼєктів не мають сенсу. Зі значеннями переліку вони завжди повертатимуть false.
Такий випадок, без повʼязаних даних, називають «чистим випадком» (Pure Case). Перелік, що містить лише чисті випадки, називають чистим переліком (Pure Enum).
Усі чисті випадки реалізовані як екземпляри свого типу переліку. Всередині тип переліку представлений як клас.
Усі випадки мають властивість name, доступну лише для читання: це чутлива до регістру назва самого випадку.
<?php
enum Suit
{
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
print Suit::Spades->name;
// prints "Spades"
Якщо назву отримано динамічно, існування випадку переліку можна перевірити або прочитати його функціями defined() і constant(). Робити так, утім, не рекомендовано: для більшості сценаріїв підійдуть підкріплені переліки.
Підкріплені переліки
Типово випадки переліку не мають скалярного відповідника. Це просто обʼєкти-одинаки. Проте часто випадок переліку має вміти «ходити туди й назад» до бази даних чи подібного сховища, тож вбудований скалярний (а отже, тривіально серіалізовний) відповідник буває корисним.
Скалярний відповідник для переліку визначають так:
<?php
enum Suit: string
{
case Hearts = 'H';
case Diamonds = 'D';
case Clubs = 'C';
case Spades = 'S';
}
Випадок зі скалярним відповідником називають підкріпленим випадком (Backed Case), бо його «підкріплює» простіше значення. Перелік, що містить лише підкріплені випадки, називають «підкріпленим переліком» (Backed Enum). Підкріплений перелік може містити лише підкріплені випадки. Чистий перелік може містити лише чисті випадки.
Підкріплений перелік може спиратися на типи int або string, і конкретний перелік підтримує лише один тип за раз (тобто жодного обʼєднання int|string). Якщо перелік позначено як такий, що має скалярний відповідник, то всі випадки мають явно визначений унікальний скалярний відповідник. Автоматично згенерованих скалярних відповідників (наприклад, послідовних цілих) немає. Підкріплені випадки мають бути унікальними; два випадки підкріпленого переліку не можуть мати однаковий скалярний відповідник. Проте константа може посилатися на випадок, фактично створюючи псевдонім. Див. Константи переліку.
Відповідні значення можуть бути константним скалярним виразом. До PHP 8.2.0 відповідні значення мали бути літералами або літеральними виразами. Це означає, що константи й константні вирази не підтримувалися. Тобто 1 + 1 було дозволено, а 1 + SOME_CONST - ні.
Підкріплені випадки мають додаткову властивість value, доступну лише для читання: це значення, вказане у визначенні.
<?php
enum Suit: string
{
case Hearts = 'H';
case Diamonds = 'D';
case Clubs = 'C';
case Spades = 'S';
}
print Suit::Clubs->value;
// Prints "C"
Щоб властивість value лишалася лише для читання, змінну не можна призначити посиланням на неї. Тобто такий код викидає помилку:
<?php
enum Suit: string
{
case Hearts = 'H';
case Diamonds = 'D';
case Clubs = 'C';
case Spades = 'S';
}
$suit = Suit::Clubs;
$ref = &$suit->value;
// Fatal Error: Cannot indirectly modify readonly property Suit::$value
Підкріплені переліки реалізують внутрішній інтерфейс BackedEnum, який надає два додаткові методи:
from(int|string): selfприймає скаляр і повертає відповідний випадок переліку. Якщо його не знайдено, метод викидаєValueError. Корисно передусім тоді, коли вхідний скаляр є довіреним, а відсутнє значення переліку варто вважати помилкою, що зупиняє застосунок.tryFrom(int|string): ?selfприймає скаляр і повертає відповідний випадок переліку. Якщо його не знайдено, повертаєnull. Корисно передусім тоді, коли вхідний скаляр не є довіреним, а викликач хоче реалізувати власну обробку помилок або логіку значення за замовчуванням.
Методи from() і tryFrom() дотримуються стандартних правил слабкої/строгої типізації. У режимі слабкої типізації передавання цілого або рядка є прийнятним, і система приведе значення відповідно. Передавання числа з рухомою крапкою теж спрацює з приведенням. У режимі строгих типів передавання цілого до from() для переліку, підкріпленого рядком (чи навпаки), призведе до TypeError, як і число з рухомою крапкою за будь-яких обставин. Усі інші типи параметрів викидають TypeError в обох режимах.
<?php
enum Suit: string
{
case Hearts = 'H';
case Diamonds = 'D';
case Clubs = 'C';
case Spades = 'S';
}
function get_stuff_from_database($id) {
return [
'suit' => 'S',
];
}
$record = get_stuff_from_database(42);
print $record['suit'] . "\n";
$suit = Suit::tryFrom('A') ?? Suit::Spades;
// Invalid data returns null, so Suit::Spades is used instead.
print $suit->value . "\n";
$suit = Suit::from('X');
// Invalid data throws a ValueError: "X" is not a valid backing scalar value for enum Suit
print $suit->value . "\n";
Власноруч визначений метод from() або tryFrom() у підкріпленому переліку призведе до фатальної помилки.
Методи переліку
Переліки (як чисті, так і підкріплені) можуть містити методи й реалізовувати інтерфейси. Якщо перелік реалізує інтерфейс, то будь-яка перевірка типу на цей інтерфейс прийматиме також усі випадки цього переліку.
<?php
interface Colorful
{
public function color(): string;
}
enum Suit implements Colorful
{
case Hearts;
case Diamonds;
case Clubs;
case Spades;
// Виконує контракт інтерфейсу.
public function color(): string
{
return match ($this) {
Suit::Hearts, Suit::Diamonds => 'Red',
Suit::Clubs, Suit::Spades => 'Black',
};
}
// Не є частиною інтерфейсу; це нормально.
public function shape(): string
{
return "Rectangle";
}
}
function paint(Colorful $c)
{
print $c->color() . "\n";
}
paint(Suit::Clubs); // Works
print Suit::Diamonds->shape(); // prints "Rectangle"
У цьому прикладі всі чотири екземпляри Suit мають два методи, color() і shape(). З погляду коду, що їх викликає, і перевірок типів вони поводяться точно так само, як будь-який інший екземпляр обʼєкта.
У підкріпленому переліку оголошення інтерфейсу йде після оголошення типу підкріплення.
<?php
interface Colorful
{
public function color(): string;
}
enum Suit: string implements Colorful
{
case Hearts = 'H';
case Diamonds = 'D';
case Clubs = 'C';
case Spades = 'S';
// Виконує контракт інтерфейсу.
public function color(): string
{
return match ($this) {
Suit::Hearts, Suit::Diamonds => 'Red',
Suit::Clubs, Suit::Spades => 'Black',
};
}
}
Усередині методу визначена змінна $this, яка посилається на екземпляр випадку.
Методи можуть бути як завгодно складними, але на практиці зазвичай повертають статичне значення або роблять match за $this, щоб дати різні результати для різних випадків.
Зауважте, що в цьому випадку кращою практикою моделювання даних було б також визначити тип-перелік SuitColor зі значеннями Red і Black та повертати його. Проте це ускладнило б приклад.
Наведена ієрархія логічно подібна до такої структури класів (хоч це й не той код, який виконується насправді):
<?php
interface Colorful
{
public function color(): string;
}
final class Suit implements UnitEnum, Colorful
{
public const Hearts = new self('Hearts');
public const Diamonds = new self('Diamonds');
public const Clubs = new self('Clubs');
public const Spades = new self('Spades');
private function __construct(public readonly string $name) {}
public function color(): string
{
return match ($this) {
Suit::Hearts, Suit::Diamonds => 'Red',
Suit::Clubs, Suit::Spades => 'Black',
};
}
public function shape(): string
{
return "Rectangle";
}
public static function cases(): array
{
// Недопустимий метод, бо визначати cases() вручну в переліку заборонено.
// Див. також розділ «Перелік значень».
}
}
Методи можуть бути public, private або protected, хоча на практиці private і protected еквівалентні, бо успадкування заборонено.
Статичні методи переліку
Переліки можуть мати й статичні методи. Статичні методи на самому переліку використовують передусім як альтернативні конструктори. Наприклад:
<?php
enum Size
{
case Small;
case Medium;
case Large;
public static function fromLength(int $cm): self
{
return match (true) {
$cm < 50 => self::Small,
$cm < 100 => self::Medium,
default => self::Large,
};
}
}
var_dump(Size::fromLength(50));
Статичні методи можуть бути public, private або protected, хоча на практиці private і protected еквівалентні, бо успадкування заборонено.
Константи переліку
Переліки можуть містити константи, які бувають public, private або protected, хоча на практиці private і protected еквівалентні, бо успадкування заборонено.
Константа переліку може посилатися на випадок переліку:
<?php
enum Size
{
case Small;
case Medium;
case Large;
public const Huge = self::Large;
}
var_dump(Size::Huge);
Трейти
Переліки можуть використовувати трейти, які поводяться так само, як у класах. Застереження: трейт, підключений у переліку через use, не повинен містити властивостей. У ньому можуть бути лише методи, статичні методи й константи. Трейт із властивостями спричинить фатальну помилку.
<?php
interface Colorful
{
public function color(): string;
}
trait Rectangle
{
public function shape(): string
{
return "Rectangle";
}
}
enum Suit implements Colorful
{
use Rectangle;
case Hearts;
case Diamonds;
case Clubs;
case Spades;
public function color(): string
{
return match ($this) {
Suit::Hearts, Suit::Diamonds => 'Red',
Suit::Clubs, Suit::Spades => 'Black',
};
}
}
$suit = Suit::Spades;
var_dump($suit->color());
var_dump($suit->shape());
Значення переліків у константних виразах
Оскільки випадки представлені як константи самого переліку, їх можна використовувати як статичні значення в більшості константних виразів: значення властивостей за замовчуванням, значення статичних змінних за замовчуванням, значення параметрів за замовчуванням, глобальні константи й константи класів. Їх не можна використовувати в значеннях інших випадків переліку, але звичайні константи можуть посилатися на випадок переліку.
Водночас неявні виклики магічних методів, як-от ArrayAccess на переліках, не дозволені у статичних або константних визначеннях, бо неможливо гарантувати, що результат детермінований, а виклик методу вільний від побічних ефектів. Виклики функцій, виклики методів і доступ до властивостей і далі є недопустимими операціями в константних виразах.
<?php
// Це цілком легальне визначення переліку.
enum Direction implements ArrayAccess
{
case Up;
case Down;
public function offsetExists($offset): bool
{
return false;
}
public function offsetGet($offset): mixed
{
return null;
}
public function offsetSet($offset, $value): void
{
throw new Exception();
}
public function offsetUnset($offset): void
{
throw new Exception();
}
}
class Foo
{
// Так можна.
const DOWN = Direction::Down;
// Так не можна, бо результат може бути недетермінованим.
const UP = Direction::Up['short'];
// Fatal error: Cannot use [] on enums in constant expression
}
// А це цілком легально, бо це не константний вираз.
$x = Direction::Up['short'];
var_dump("\$x is " . var_export($x, true));
$foo = new Foo();
Відмінності від обʼєктів
Хоч переліки й побудовані на класах та обʼєктах, вони підтримують не всю функціональність, повʼязану з обʼєктами. Зокрема, випадкам переліку заборонено мати стан.
- Конструктори й деструктори заборонені.
- Успадкування не підтримується. Переліки не можуть розширювати інші типи, і їх не можна розширювати.
- Статичні властивості й властивості обʼєкта не дозволені.
- Клонування випадку переліку не підтримується, бо випадки мають бути екземплярами-одинаками.
- Магічні методи, окрім перелічених нижче, заборонені.
- Переліки завжди мають бути оголошені перед використанням.
Доступна така обʼєктна функціональність, і поводиться вона так само, як у будь-якому іншому обʼєкті:
- Public-, private- і protected-методи.
- Public-, private- і protected-статичні методи.
- Public-, private- і protected-константи.
- Переліки можуть реалізовувати будь-яку кількість інтерфейсів.
- Переліки та випадки можуть мати прикріплені атрибути. Цільовий фільтр
TARGET_CLASSохоплює самі переліки. Цільовий фільтрTARGET_CLASS_CONSTохоплює випадки переліку. - Магічні методи
__call,__callStaticта__invoke. - Константи
__CLASS__і__FUNCTION__поводяться як звичайно.
Магічна константа ::class для типу-переліку обчислюється в назву типу разом із простором імен, точно як для обʼєкта. Магічна константа ::class на екземплярі випадку теж обчислюється в тип переліку, бо випадок є екземпляром цього типу.
Крім того, випадки переліку не можна створити напряму через new, ані через ReflectionClass::newInstanceWithoutConstructor() у рефлексії. Обидва варіанти дадуть помилку.
<?php
$clovers = new Suit();
// Error: Cannot instantiate enum Suit
$horseshoes = (new ReflectionClass(Suit::class))->newInstanceWithoutConstructor()
// Error: Cannot instantiate enum Suit
Перелік значень
І чисті, і підкріплені переліки реалізують внутрішній інтерфейс UnitEnum. UnitEnum містить статичний метод cases(). cases() повертає щільний масив усіх визначених випадків у порядку оголошення.
<?php
enum Suit
{
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
var_dump(Suit::cases());
enum SuitBacked: string
{
case Hearts = 'H';
case Diamonds = 'D';
case Clubs = 'C';
case Spades = 'S';
}
var_dump(SuitBacked::cases());
Власноруч визначений метод cases() у переліку призведе до фатальної помилки.
Серіалізація
Переліки серіалізуються інакше, ніж обʼєкти. Зокрема, для них є новий код серіалізації, "E", який задає назву випадку переліку. Процедура десеріалізації може використати її, щоб присвоїти змінній наявне значення-одинак. Це гарантує, що:
<?php
enum Suit: string
{
case Hearts = 'H';
case Diamonds = 'D';
case Clubs = 'C';
case Spades = 'S';
}
Suit::Hearts === unserialize(serialize(Suit::Hearts));
print serialize(Suit::Hearts);
// E:11:"Suit:Hearts";
Під час десеріалізації, якщо для серіалізованого значення не вдається знайти відповідні перелік і випадок, буде видано попередження й повернуто false.
Опція allowed_classes функції unserialize() не впливає на переліки.
Якщо чистий перелік серіалізувати в JSON, буде викинуто помилку. Якщо в JSON серіалізувати підкріплений перелік, його буде представлено лише скалярним значенням відповідного типу. Обидві поведінки можна перевизначити, реалізувавши JsonSerializable.
Для print_r() вивід випадку переліку трохи відрізняється від обʼєктів, щоб менше плутати.
<?php
enum Foo
{
case Bar;
}
enum Baz: int
{
case Beep = 5;
}
print_r(Foo::Bar);
print_r(Baz::Beep);
/* Produces
Foo Enum (
[name] => Bar
)
Baz Enum:int {
[name] => Beep
[value] => 5
}
*/
Чому переліки не можна розширювати
У класів контракти стосуються методів:
<?php
class A {}
class B extends A {}
function foo(A $a) {}
function bar(B $b)
{
foo($b);
}
Цей код типобезпечний, бо B дотримується контракту A, і завдяки ко/контраваріантності будь-які очікування щодо методів збережуться, за винятком винятків.
У переліків контракти стосуються випадків, а не методів:
<?php
enum ErrorCode
{
case SOMETHING_BROKE;
}
function quux(ErrorCode $errorCode)
{
// На момент написання здається, що цей код покриває всі випадки
match ($errorCode) {
ErrorCode::SOMETHING_BROKE => true,
};
}
Вираз match у функції quux можна статично проаналізувати як такий, що покриває всі випадки ErrorCode.
Але уявімо, що переліки дозволено розширювати:
<?php
// Уявний експеримент: код, у якому переліки не final.
// Зауважте, у PHP це насправді не працюватиме.
enum MoreErrorCode extends ErrorCode
{
case PEBKAC;
}
function fot(MoreErrorCode $errorCode)
{
quux($errorCode);
}
fot(MoreErrorCode::PEBKAC);
За звичайними правилами успадкування клас, що розширює інший, пройде перевірку типу.
Проблема в тому, що вираз match у quux() більше не покриває всі випадки. Оскільки він не знає про MoreErrorCode::PEBKAC, match викине виняток.
Через це переліки є final і їх не можна розширювати.
Приклади
Приклад 1. Базові обмежені значення
<?php
enum SortOrder
{
case Asc;
case Desc;
}
function query($fields, $filter, SortOrder $order = SortOrder::Asc)
{
/* ... */
}
Функція query() тепер може працювати спокійно, знаючи, що $order гарантовано дорівнює або SortOrder::Asc, або SortOrder::Desc. Будь-яке інше значення дало б TypeError, тож додаткових перевірок чи тестів не потрібно.
Приклад 2. Складніші взаємовиключні значення
<?php
enum UserStatus: string
{
case Pending = 'P';
case Active = 'A';
case Suspended = 'S';
case CanceledByUser = 'C';
public function label(): string
{
return match ($this) {
self::Pending => 'Pending',
self::Active => 'Active',
self::Suspended => 'Suspended',
self::CanceledByUser => 'Canceled by user',
};
}
}
$status = UserStatus::Suspended;
var_dump($status->label());
У цьому прикладі статус користувача може бути одним і тільки одним із UserStatus::Pending, UserStatus::Active, UserStatus::Suspended або UserStatus::CanceledByUser. Функція може типізувати параметр як UserStatus і далі приймати лише ці чотири значення, і крапка.
Усі чотири значення мають метод label(), який повертає зрозумілий людині рядок. Цей рядок не залежить від скалярного відповідника - «машинної назви», яку можна використати, наприклад, у полі бази даних чи в HTML-елементі select.
<?php
enum UserStatus: string
{
case Pending = 'P';
case Active = 'A';
case Suspended = 'S';
case CanceledByUser = 'C';
public function label(): string
{
return match($this) {
self::Pending => 'Pending',
self::Active => 'Active',
self::Suspended => 'Suspended',
self::CanceledByUser => 'Canceled by user',
};
}
}
foreach (UserStatus::cases() as $case) {
printf(
"<option value=\"%s\">%s</option>\n",
htmlentities($case->value),
htmlentities($case->label())
);
}
Перекладаємо з офіційної документації, розділ за розділом, і не ховаємо недоперекладене. Помітили неточність у терміні чи реченні: напишіть, виправимо.