Подтвердить что ты не робот

Почему вы не можете наследовать от еще не определенного класса, который наследуется от еще не определенного класса?

Я изучаю компиляцию классов, последовательность и логику.

Если я объявляю класс перед простым родителем:

 class First extends Second{}
 class Second{}

Это будет работать нормально. См. живые примеры в версиях PHP.

Но если родительский класс также имеет некоторых еще не объявленных родителей (расширяет или реализует), как в этом примере:

class First extends Second{}
class Second extends Third{}
class Third{}

У меня будет ошибка:

Неустранимая ошибка: Класс "Второй" не найден...

См. живой пример в версиях PHP.

Итак, почему во втором примере он не может найти класс Second? Может быть, php не может скомпилировать этот класс, потому что ему нужно также компилировать класс Third, или что?

Я пытаюсь выяснить, почему в первом примере PHP компилирует класс Second, но если он будет иметь некоторые родительские классы, это не будет. Я много исследовал, но ничего точно.

  • Я не пытаюсь писать код таким образом, но в этом примере я пытаюсь понять, как работает компиляция и ее последовательность.
4b9b3361

Ответ 1

Итак, PHP использует что-то, называемое "поздним связыванием". В принципе, наследование и определение класса не происходит до конца компиляции файла.

Для этого есть несколько причин. Первый пример, который вы показали (first extends second {} working). Вторая причина - opcache.

Чтобы компиляция работала корректно в области opcache, компиляция должна происходить без состояния из других скомпилированных файлов. Это означает, что при компиляции файла таблица символов класса очищается.

Затем результат этой компиляции кэшируется. Затем во время выполнения, когда скомпилированный файл загружается из памяти, opcache запускает последнее связывание, которое затем наследует и фактически объявляет классы.

class First {}

Когда этот класс отображается, он сразу же добавляется в таблицу символов. Независимо от того, где он находится в файле. Потому что нет необходимости в позднем связывании чего-либо, это уже полностью определено. Этот метод называется ранним связыванием, и это позволяет вам использовать класс или функцию до ее объявления.

class Third extends Second {}

Когда это видно, оно скомпилировано, но фактически не объявлено. Вместо этого он добавлен в список "позднего связывания".

class Second extends First {}

Когда это наконец увидится, оно тоже скомпилировано и не объявлено. Он добавлен в список последних привязок, но после Third.

Итак, теперь, когда происходит поздний процесс привязки, он поочередно проходит список "поздних связанных" классов. Первый, который он видит, - Third. Затем он пытается найти класс Second, но не может (поскольку он пока еще не объявлен). Таким образом, ошибка возникает.

Если вы переустановите классы:

class Second extends First {}
class Third extends Second {}
class First {}

Тогда вы увидите, что он отлично работает.

Зачем вообще это делать?

Ну, PHP смешно. Представьте себе несколько файлов:

<?php // a.php
class Foo extends Bar {}

<?php // b1.php
class Bar {
    //impl 1
}

<?php // b2.php
class Bar {
    //impl 2
}

Теперь, какой экземпляр Foo, который вы получите, будет зависеть от того, какой файл b вы загрузили. Если вам требуется b2.php, вы получите Foo extends Bar (impl2). Если вам требуется b1.php, вы получите Foo extends Bar (impl1).

Обычно мы не пишем код таким образом, но есть несколько случаев, когда это может произойти.

В обычном PHP-запросе это тривиально. Причина в том, что мы можем знать о Bar, пока мы компилируем Foo. Таким образом, мы можем соответствующим образом скорректировать наш процесс компиляции.

Но когда мы добавляем кеш-код в коде, все становится намного сложнее. Если мы скомпилировали Foo с глобальным состоянием b1.php, то позже (в другом запросе) переключится на b2.php, все будет разбито странными способами.

Таким образом, вместо того, чтобы компилировать файл, кеши opcode обнуляют глобальное состояние. Таким образом, a.php будет скомпилирован, как если бы это был единственный файл в приложении.

После компиляции он кэшируется в память (для повторного использования более поздними запросами).

Затем после этой точки (или после ее загрузки из памяти в будущем запросе) выполняются шаги с задержкой. Затем он связывает скомпилированный файл с состоянием запроса.

Таким образом, opcache может более эффективно кэшировать файлы как независимые объекты, поскольку привязка к глобальному состоянию происходит после чтения кэша.

Исходный код.

Чтобы понять, почему, давайте посмотрим на исходный код.

В Zend/zend_compile.c мы можем увидеть функцию, которая компилирует класс: zend_compile_class_decl(). Примерно наполовину вниз вы увидите следующий код:

if (extends_ast) {
    opline->opcode = ZEND_DECLARE_INHERITED_CLASS;
    opline->extended_value = extends_node.u.op.var;
} else {
    opline->opcode = ZEND_DECLARE_CLASS;
}

Поэтому он изначально выдает код операции, чтобы объявить унаследованный класс. Затем, после компиляции, вызывается функция под названием zend_do_early_binding(). Это предварительно объявляет функции и классы в файле (поэтому они доступны вверху). Для обычных классов и функций он просто добавляет их в таблицу символов (объявляет их).

Интересный бит находится в унаследованном случае:

if (((ce = zend_lookup_class_ex(Z_STR_P(parent_name), parent_name + 1, 0)) == NULL) ||
    ((CG(compiler_options) & ZEND_COMPILE_IGNORE_INTERNAL_CLASSES) &&
    (ce->type == ZEND_INTERNAL_CLASS))) {
    if (CG(compiler_options) & ZEND_COMPILE_DELAYED_BINDING) {
        uint32_t *opline_num = &CG(active_op_array)->early_binding;

        while (*opline_num != (uint32_t)-1) {
            opline_num = &CG(active_op_array)->opcodes[*opline_num].result.opline_num;
        }
        *opline_num = opline - CG(active_op_array)->opcodes;
        opline->opcode = ZEND_DECLARE_INHERITED_CLASS_DELAYED;
        opline->result_type = IS_UNUSED;
        opline->result.opline_num = -1;
    }
    return;
}

Внешний, если в основном пытается извлечь класс из таблицы символов и проверяет, не существует ли он. Второй, если проверяет, используем ли мы задержку привязки (opcache включен).

Затем он копирует код операции для объявления класса в задержанный ранний блок привязки.

Наконец, вызывается функция zend_do_delayed_early_binding() (обычно с помощью opcache), которая проходит через список и фактически связывает унаследованные классы:

while (opline_num != (uint32_t)-1) {
    zval *parent_name = RT_CONSTANT(op_array, op_array->opcodes[opline_num-1].op2);
    if ((ce = zend_lookup_class_ex(Z_STR_P(parent_name), parent_name + 1, 0)) != NULL) {
        do_bind_inherited_class(op_array, &op_array->opcodes[opline_num], EG(class_table), ce, 0);
    }
    opline_num = op_array->opcodes[opline_num].result.opline_num;
}

TL; DR

Заказ не имеет значения для классов, которые не распространяют другой класс.

Любой класс, который расширяется, должен быть определен до того, как он был реализован (или должен использоваться автозагрузчик).