Enum’ы в PHP с нами уже давно, но вы задумывались, как они реально работают внутри? Давайте разберёмся, что там происходит под капотом.

А под капотом enum - это почти обычный класс, помеченный специальным флагом, что роднит его с интерфейсами, трейтами и анонимными классами.
/* Special class types*/
#define ZEND_ACC_INTERFACE (1 << 0)
#define ZEND_ACC_TRAIT (1 << 1)
#define ZEND_ACC_ANON_CLASS (1 << 2)
#define ZEND_ACC_ENUM (1 << 28)
Почему 28, а не 3? Дело в том, что помимо этих 4-х, у класса ещё много других флагов и большая часть диапазона 3..27 уже занята. Мало того, флагов уже такое количество, что буквально на днях(30.09.25) в мастере появился коммит с таким пояснением:
New zend_class_entry.ce_flags2 and zend_function.fn_flags2 fields were added, given the primary flags were running out of bits.
Фактически, компиляция enum ничем не отличается от компиляции обычного класса, кроме дополнительных проверок и ограничений, описанных в документации, обработки ключевого слова case и построения карты соответствия этих кейсов значениям(для реализации методов from и tryFrom), если enum типизированный.
Сами кейсы совершенно банальным образом преобразуются в константы класса, помеченные специальным флагом.
zend_class_constant *c = zend_declare_class_constant_ex(enum_class, enum_case_name, &value_zv, ZEND_ACC_PUBLIC, doc_comment);
ZEND_CLASS_CONST_FLAGS(c) |= ZEND_CLASS_CONST_IS_CASE;
Основное же отличие от обычных констант заключается в том, что константам перечисления присваиваются не скалярные значения, а инстансы класса в котором они объявлены. При этом каждому созданному экземпляру выставляется два параметра: имя константы и её значение(или null, если enum не типизированный).
Кроме того, как можно увидеть по коду, эти инстансы помечаются как GC_NOT_COLLECTABLE, а их свойствам сбрасываются все флаги, чтобы с их нельзя было изменить в рантайме.
zend_object *zend_enum_new(zval *result, zend_class_entry *ce, zend_string *case_name, zval *backing_value_zv)
{
zend_object *zobj = zend_objects_new(ce);
GC_ADD_FLAGS(zobj, GC_NOT_COLLECTABLE);
ZVAL_OBJ(result, zobj);
zval *zname = OBJ_PROP_NUM(zobj, 0);
ZVAL_STR_COPY(zname, case_name);
/* ZVAL_COPY does not set Z_PROP_FLAG, this needs to be cleared to avoid leaving IS_PROP_REINITABLE set */
Z_PROP_FLAG_P(zname) = 0;
if (backing_value_zv != NULL) {
zval *prop = OBJ_PROP_NUM(zobj, 1);
ZVAL_COPY(prop, backing_value_zv);
/* ZVAL_COPY does not set Z_PROP_FLAG, this needs to be cleared to avoid leaving IS_PROP_REINITABLE set */
Z_PROP_FLAG_P(prop) = 0;
}
return zobj;
}
И вот как раз такая реализация позволяет, с одной стороны, использовать их там, где допустимы только constant expressions:
enum MyEnum: int
{
case FIRST = 1;
case SECOND = 2;
}
class MyClass
{
const order = MyEnum::FIRST;
}
А с другой, творить с enum-ами странные вещи, типа таких:
enum MyEnum
{
case FIRST;
case SECOND;
}
var_dump(MyEnum::FIRST::SECOND::FIRST::SECOND);
----------------------------
enum(MyEnum::SECOND)
И вот как это безобразие выглядит в опкодах:
line #* E I O op ext return operands
----------------------------------------------------------------------
6 0 E > DECLARE_CLASS 'myenum'
12 1 INIT_FCALL 'var_dump'
2 FETCH_CLASS_CONSTANT ~0 'MyEnum', 'FIRST'
3 FETCH_CLASS 0 $1 ~0
4 FETCH_CLASS_CONSTANT ~2 $1, 'SECOND'
5 FETCH_CLASS 0 $3 ~2
6 FETCH_CLASS_CONSTANT ~4 $3, 'FIRST'
7 FETCH_CLASS 0 $5 ~4
8 FETCH_CLASS_CONSTANT ~6 $5, 'SECOND'
9 SEND_VAL ~6
10 DO_ICALL
14 11 > RETURN 1
Сама собой напрашивается оптимизация. Думаю скоро появится.
Осталось только добавить рекомендацию по сравнению enum-ов. Так как сравниваются два объекта, то строгое сравнение проведёт всего три проверки, без дополнительных действий (проверит, что типы идентичны, выберет бранч IS_OBJECT и сравнит указатели на объекты):
ZEND_API bool ZEND_FASTCALL zend_is_identical(const zval *op1, const zval *op2)
{
if (Z_TYPE_P(op1) != Z_TYPE_P(op2)) {
return 0;
}
switch (Z_TYPE_P(op1)) {
...
case IS_OBJECT:
return (Z_OBJ_P(op1) == Z_OBJ_P(op2));
...
}
Тогда как при не строгом сравнении логика будет не сильно, но сложнее.
ZEND_API int ZEND_FASTCALL zend_compare(zval *op1, zval *op2) /* {{{ */
{
while (1) {
switch (TYPE_PAIR(Z_TYPE_P(op1), Z_TYPE_P(op2))) {
...
default:
if (Z_ISREF_P(op1)) {
op1 = Z_REFVAL_P(op1);
continue;
} else if (Z_ISREF_P(op2)) {
op2 = Z_REFVAL_P(op2);
continue;
}
if (Z_TYPE_P(op1) == IS_OBJECT || Z_TYPE_P(op2) == IS_OBJECT) {
zval *object, *other;
if (Z_TYPE_P(op1) == IS_OBJECT) {
object = op1;
other = op2;
} else {
object = op2;
other = op1;
}
if (EXPECTED(Z_TYPE_P(other) == IS_OBJECT)) {
if (Z_OBJ_P(object) == Z_OBJ_P(other)) {
return 0;
}
} else
.......
PS Осталось дождаться дженериков
Satorlous
Хорошая шутка
rjhdby Автор
Это не шутка, а надежда :)