Ответ 1
Отказ от ответственности: я не утверждаю, что понимаю внутреннюю работу Zend. Ниже приводится моя интерпретация источника PHP, которая в значительной степени подпитывается образованными догадками. Хотя я полностью уверен в заключении, терминология или детали могут быть отключены. Мне бы очень хотелось услышать от кого-либо, у кого есть опыт в работе с Zend по этому вопросу.
Исследование
Из парсера PHP мы можем видеть, что при обнаружении объявления класса zend_do_early_binding
. Здесь - это код, который обрабатывает объявление производных классов:
case ZEND_DECLARE_INHERITED_CLASS:
{
zend_op *fetch_class_opline = opline-1;
zval *parent_name;
zend_class_entry **pce;
parent_name = &CONSTANT(fetch_class_opline->op2.constant);
if ((zend_lookup_class(Z_STRVAL_P(parent_name), Z_STRLEN_P(parent_name), &pce TSRMLS_CC) == FAILURE) ||
((CG(compiler_options) & ZEND_COMPILE_IGNORE_INTERNAL_CLASSES) &&
((*pce)->type == ZEND_INTERNAL_CLASS))) {
if (CG(compiler_options) & ZEND_COMPILE_DELAYED_BINDING) {
zend_uint *opline_num = &CG(active_op_array)->early_binding;
while (*opline_num != -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 (do_bind_inherited_class(CG(active_op_array), opline, CG(class_table), *pce, 1 TSRMLS_CC) == NULL) {
return;
}
/* clear unnecessary ZEND_FETCH_CLASS opcode */
zend_del_literal(CG(active_op_array), fetch_class_opline->op2.constant);
MAKE_NOP(fetch_class_opline);
table = CG(class_table);
break;
}
Этот код сразу вызывает zend_lookup_class
, чтобы узнать, существует ли родительский класс в таблице символов... и затем расходится в зависимости от того, родитель найден или нет.
Сначала посмотрим, что он делает, если найден родительский класс:
if (do_bind_inherited_class(CG(active_op_array), opline, CG(class_table), *pce, 1 TSRMLS_CC) == NULL) {
return;
}
Переходя к do_bind_inherited_class
, мы видим, что последний аргумент (который в этом вызове 1
) называется compile_time
, Это звучит интересно. Что он делает с этим аргументом?
if (compile_time) {
op1 = &CONSTANT_EX(op_array, opline->op1.constant);
op2 = &CONSTANT_EX(op_array, opline->op2.constant);
} else {
op1 = opline->op1.zv;
op2 = opline->op2.zv;
}
found_ce = zend_hash_quick_find(class_table, Z_STRVAL_P(op1), Z_STRLEN_P(op1), Z_HASH_P(op1), (void **) &pce);
if (found_ce == FAILURE) {
if (!compile_time) {
/* If we're in compile time, in practice, it quite possible
* that we'll never reach this class declaration at runtime,
* so we shut up about it. This allows the if (!defined('FOO')) { return; }
* approach to work.
*/
zend_error(E_COMPILE_ERROR, "Cannot redeclare class %s", Z_STRVAL_P(op2));
}
return NULL;
} else {
ce = *pce;
}
Хорошо... поэтому он считывает имена родительских и производных классов либо из статического (с точки зрения пользователя PHP), либо динамического контекста, в зависимости от состояния compile_time
. Затем он пытается найти запись класса ( "ce" ) в таблице классов, а если она не найдена... она возвращается, не делая ничего во время компиляции, но испускает фатальную ошибку во время выполнения.
Это звучит чрезвычайно важно. Вернемся к zend_do_early_binding
. Что он делает, если родительский класс не найден?
if (CG(compiler_options) & ZEND_COMPILE_DELAYED_BINDING) {
zend_uint *opline_num = &CG(active_op_array)->early_binding;
while (*opline_num != -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;
Кажется, что он генерирует коды операций, которые снова вызовет вызов до do_bind_inherited_class
- но на этот раз значение compile_time
будет 0
(false).
Наконец, как насчет реализации функции class_exists
PHP? Глядя на источник, вы видите этот фрагмент:
found = zend_hash_find(EG(class_table), name, len+1, (void **) &ce);
Отлично! Эта переменная class_table
является тем же самым class_table
, которая участвует в вызове do_bind_inherited_class
, который мы видели ранее! Поэтому возвращаемое значение class_exists
зависит от того, была ли запись для класса уже вставлена в class_table
на do_bind_inherited_class
.
Выводы
Компилятор Zend не работает с директивами include
во время компиляции (даже если имя файла жестко запрограммировано).
Если бы это было так, то не было бы причины испускать факультативную ошибку повторной декларации класса, основанную на флаге compile_time
, который не задан; ошибка может быть испущена безоговорочно.
Когда компилятор встречает объявление производного класса, где базовый класс не был объявлен в том же файле script, он подталкивает акт регистрации класса во внутренних структурах данных во время выполнения.
Это видно из последнего фрагмента кода выше, который устанавливает код операции ZEND_DECLARE_INHERITED_CLASS_DELAYED
для регистрации класса при выполнении script. В этот момент флаг compile_time
будет false
, и поведение будет немного отличаться.
Возвращаемое значение class_exists
зависит от того, был ли класс уже зарегистрирован.
Так как это происходит по-разному во время компиляции и во время выполнения, поведение class_exists
также отличается:
- классы, чьи предки все включены в один и тот же исходный файл, регистрируются во время компиляции; они существуют и могут быть созданы в любой точке, в которой script
- классы, у которых есть предок, определенный в другом исходном файле, регистрируются во время выполнения; до того, как VM выполнит коды операций, которые соответствуют определению класса в источнике, эти классы не существуют для всех практических целей (
class_exists
возвращаетfalse
, экземпляр дает фатальную ошибку)