Я исследую составление классов, их последовательность и логику.
Если я объявляю класс перед простым родителем:
class First extends Second{}
class Second{}
Это будет работать хорошо. Смотрите живой пример через версии PHP.
Но если родительский класс также имеет некоторых еще не объявленных родителей (расширяет или реализует), как в этом примере:
class First extends Second{}
class Second extends Third{}
class Third{}
У меня будет ошибка:
Неустранимая ошибка: класс «Второй» не найден …
Смотрите живой пример через версии PHP.
Итак, почему во втором примере это не может найти Second
учебный класс?
Может быть, PHP не может скомпилировать этот класс, потому что он должен также скомпилировать Third
класс или что?
Я пытаюсь выяснить, почему в первом примере PHP компилируется класс Second, но если он будет иметь некоторые родительские классы, он не будет. Я много исследовал, но точно ничего.
Итак, PHP использует то, что называется «поздним связыванием». По сути, наследование и определение класса не происходит до конца компиляции файла.
Есть ряд причин для этого. Первый пример, который вы показали (first extends second {}
за работой). Вторая причина — 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
что-то сломалось бы странным образом.
Поэтому вместо этого код операции кэширует нулевое глобальное состояние до компиляции файла. Так 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;
}
Внешний if в основном пытается извлечь класс из таблицы символов и проверяет, не существует ли он. Второй if проверяет, используем ли мы отложенное связывание (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;
}
Порядок не имеет значения для классов, которые не расширяют другой класс.
Любой класс, который расширяется должен быть определенным до точки, в которой это реализовано (или должен использоваться автозагрузчик).
Других решений пока нет …