免责声明:我不声称理解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;
}
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) {
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)
*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
变量与我们之前看到的do_bind_inherited_class
调用中涉及的class_table
是相同的!因此,class_exists
的返回值取决于是否已经通过do_bind_inherited_class
将类的条目插入到class_table
中。
结论
Zend编译器不会在编译时处理include
指令(即使文件名是硬编码的)。
如果它这样做了,那么就没有理由根据未设置compile_time
标志来发出类重新声明致命错误;该错误可以无条件地发出。
当编译器遇到一个派生类声明,其中基类没有在同一脚本文件中声明时,它会将在运行时注册类的操作推迟到运行时。
这可以从上面的最后一个代码片段中看出,它设置了一个ZEND_DECLARE_INHERITED_CLASS_DELAYED
操作码,以在执行脚本时注册类。此时,compile_time
标志将为false
,行为将略有不同。
class_exists
的返回值取决于类是否已经被注册。
由于这在编译时和运行时以不同的方式发生,class_exists
的行为也不同:
- 所有祖先都包含在同一源文件中的类在编译时注册;它们存在并且可以在该脚本的任何时刻实例化。
- 在其他源文件中定义了祖先的类在运行时注册;在VM执行与源代码中的类定义相对应的操作码之前,这些类在实际目的上不存在(
class_exists
返回false
,实例化会导致致命错误)。
A
在同一个文件中定义,那么BA(之前)也存在。 - Jon