免责声明:我并不声称了解 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's 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(假)。
最后,class_existsPHP函数的实现呢?查看源代码显示了这个片段:
found = zend_hash_find(EG(class_table), name, len+1, (void **) &ce);
伟大的!这个class_table变量与我们之前看到的调用class_table中涉及的变量相同!do_bind_inherited_class因此 的返回值class_exists取决于该类的条目是否已经被class_tableby插入do_bind_inherited_class。
结论
Zend 编译器在编译时不作用于include指令(即使文件名是硬编码的)。
如果是这样,那么就没有理由根据compile_time未设置的标志发出类重新声明致命错误;可以无条件地发出错误。
当编译器遇到未在同一脚本文件中声明基类的派生类声明时,它会将在其内部数据结构中注册类的行为推送到运行时。
从上面的最后一个代码片段可以看出这一点,它设置了一个ZEND_DECLARE_INHERITED_CLASS_DELAYED操作码来在脚本执行时注册类。到那时,compile_time旗帜false和行为将略有不同。
的返回值class_exists取决于该类是否已经注册。
由于这在编译时和运行时以不同的方式发生,所以行为class_exists也不同:
- 其祖先都包含在同一源文件中的类在编译时注册;它们存在并且可以在该脚本中的任何位置实例化
- 在另一个源文件中定义了祖先的类在运行时注册;在 VM 执行与源中的类定义相对应的操作码之前,这些类并不存在于所有实际目的(class_exists返回false,实例化会产生致命错误)