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

Комментарии (2)


  1. Satorlous
    03.10.2025 17:34

    Осталось дождаться дженериков

    Хорошая шутка


    1. rjhdby Автор
      03.10.2025 17:34

      Это не шутка, а надежда :)