同一文件中稍后定义的派生类“不存在”?

18

假设我们有两个 PHP 文件,a.php 和 b.php。以下是文件 a.php 的内容:

<?php // content of a.php
class A {
}

以下是 b.php 文件的内容:

<?php  // content of b.php
include dirname(__FILE__) . "/a.php";
echo "A: ", class_exists("A") ? "exists" : "doesn’t exist", "\n";
echo "B: ", class_exists("B") ? "exists" : "doesn’t exist", "\n";
echo "BA (before): ", class_exists("BA") ? "exists" : "doesn’t exist", "\n";
echo "BB: ", class_exists("BB") ? "exists" : "doesn’t exist", "\n";
class B {
}
class BA extends A {
}
class BB extends B {
}
echo "BA (after): ", class_exists("BA") ? "exists" : "doesn’t exist", "\n";

如果您启动b.php脚本,则会得到以下输出:
A: exists
B: exists
BA (before): doesn’t exist
BB: exists
BA (after): exists

为什么BA类只能在类定义之后存在?而其他类甚至在它们的定义之前就存在了?这有什么区别?我希望在这两种情况下都有一个共同的行为...是否有办法在其定义之前就使用BA类?
谢谢 Michele

你使用的 PHP 版本是什么?我无法复制这个错误。 - Baba
请在此处查看类的抽象 http://php.net/manual/zh/language.oop5.abstract.php - Bud Damyanov
非常好的问题!也许你应该补充说明,如果类A在同一个文件中定义,那么BA(之前)也存在。 - Jon
@Baba:我的PHP版本是5.3.15。 - Michele Locati
@MicheleLocati:我刚刚完成了调查并添加了答案。非常感谢您提出如此有趣的问题! - Jon
显示剩余2条评论
2个回答

7

免责声明:我不声称理解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(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,实例化会导致致命错误)。

非常感谢您的深入探索。无论如何,当您说“Zend编译器在编译时不会处理包含指令”时,我问自己为什么。我们可以认为这是一个错误吗? - Michele Locati
@MicheleLocati:在我看来并不是这样——文件名可能是一个变量,因此无法在编译时包含该文件。因此,您需要两个单独的路径,并且会出现不一致的行为,更不用说可能会由此产生的错误了。 - Jon
是的,你说得对。不是 bug,但是“最终用户”的行为仍然有些愚蠢。顺便说一下,非常感谢! - Michele Locati
使用自动加载时,get_class() 和实例化将首先触发此函数,然后失败。通常情况下,自动加载程序应该能够加载类,并且即使缺少的类尚未被包含(因为它们现在被包含了),两者都应该显示其预期行为。这适用于出现在implementsextends语句中的类/接口。不过,我喜欢你的解释 :) - KingCrunch

1

这与PHP处理包含文件中的类有关:include dirname(__FILE__) . "/a.php";

BB存在是因为它扩展了在同一文件中定义的B

BA不存在,因为PHP没有解析在线调用的A

两者都可以返回相同的结果。

使用class BA extends B

include dirname(__FILE__) . "/a.php";
echo "BA (before): ", class_exists("BA") ? "exists" : "doesn’t exist", "\n";
echo "BB: ", class_exists("BB") ? "exists" : "doesn’t exist", "\n";
class B {
}
class BA extends B {
}
class BB extends B {
}
echo "BA (after): ", class_exists("BA") ? "exists" : "doesn’t exist", "\n";

或者定义 class A 并使用 class BA extends A

class A {
}
echo "<pre>";
echo "BA (before): ", class_exists("BA") ? "exists" : "doesn’t exist", "\n";
echo "BB: ", class_exists("BB") ? "exists" : "doesn’t exist", "\n";
class B {
}
class BA extends A {
}
class BB extends B {
}
echo "BA (after): ", class_exists("BA") ? "exists" : "doesn’t exist", "\n";

输出

BA (before): exists
BB: exists
BA (after): exists

结论

来自 PHP 文档的表单

当一个文件被包含时,它所包含的代码会继承该行所在文件的变量作用域。在调用文件中可用于该行的任何变量将从那一点开始在被调用的文件中可用。然而,在所包含的文件中定义的所有函数和类都具有全局作用域。

我认为 PHP 文档已经涵盖了扩展类的内容,这可以被视为需要纠正的错误,但在主要情况下,请在调用或使用类之前先包含它们。


1
这个“BUG”现在已经有7年的历史了 ;) 我想我们可以放心地说...带有extends的类永远不会摆脱这个问题。 - IncredibleHat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接