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):
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 Осталось дождаться дженериков
