【问题标题】:Derived class defined later in the same file "does not exist"?稍后在同一文件中定义的派生类“不存在”?
【发布时间】:2012-09-18 23:47:47
【问题描述】:

假设我们有两个 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 类?

谢谢

米歇尔

【问题讨论】:

  • 你用的是什么版本的PHP??我无法复制此错误
  • 非常好的问题!也许您应该补充一点,如果在同一个文件中定义了类 A,那么 BA(之前)也存在。
  • @Baba: 我的php版本在5.3.15
  • @Jon: 这就是我添加 BB 类的原因

标签: php class include require extends


【解决方案1】:

免责声明:我并不声称了解 Zend 的内部工作原理。以下是我对 PHP 源代码的解释,很大程度上是由有根据的猜测推动的。尽管我对结论完全有信心,但术语或细节可能会有所偏差。我很想听听任何在 Zend 内部有经验的人就此事发表意见。

调查

通过 PHP 解析器 we can see,当遇到类声明时,将调用 zend_do_early_binding 函数。 Here是处理派生类声明的代码:

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”),如果 not 找到那么... 它在编译时不做任何事情就返回,但会发出一个运行时出现致命错误

这听起来非常重要。让我们回到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;

它似乎再次生成will trigger a calldo_bind_inherited_class 的操作码——但这一次,compile_time 的值将是0 (false)。

最后,class_exists PHP 函数的实现呢?查看源代码显示了这个sn-p:

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 标志而发出类重新声明致命错误;可以无条件地发出错误。

当编译器遇到未在同一脚本文件中声明基类的派生类声明时,它会将在其内部数据结构中注册类的行为推送到运行时。

从上面的最后一个代码 sn-p 可以看出这一点,它设置了一个ZEND_DECLARE_INHERITED_CLASS_DELAYED 操作码来在脚本执行时注册类。届时compile_time 标志将为false,并且行为会略有不同。

class_exists的返回值取决于该类是否已经注册。

由于这在编译时和运行时以不同的方式发生,class_exists 的行为也不同:

  • 其祖先都包含在同一个源文件中的类在编译时注册;它们存在并且可以在该脚本中的任何位置实例化
  • 在另一个源文件中定义了祖先的类在运行时注册;在 VM 执行与源中的类定义对应的操作码之前,这些类并不存在于所有实际用途中(class_exists 返回false,实例化会导致致命错误)

【讨论】:

  • 非常感谢您的深入探索。无论如何,当你说“Zend 编译器在编译时不作用于包含指令”时,我问自己为什么。我们可以认为这是一个错误吗?
  • @MicheleLocati:恕我直言,文件名可能是一个变量,在这种情况下,不可能在编译时包含该文件。因此,您需要两条独立的路径,并且您的行为会不一致,更不用说这种行为变化可能导致的错误了。
  • 是的,你是对的。不是错误...但是“最终用户”的行为仍然有些愚蠢。顺便说一句,非常感谢!
  • 自动加载 get_class() 和实例化将首先触发这个,然后再失败。通常自动加载器应该能够加载到类,并且即使 mssing 类尚未包含(因为它们包含 now),两者都应该显示它们的预期行为。这适用于出现在implements- 或extends-statements 中的类/接口。不过,我喜欢你的解释:)
【解决方案2】:

这与包含文件include dirname(__FILE__) . "/a.php";中的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 文档中,这可以被视为需要纠正的 BUG,但主要是在调用或使用它们之前包含你的类

【讨论】:

  • 这个“BUG”现在已经 7 岁了 ;) 我想我们可以有把握地说...extends 的课程永远不会摆脱这个问题。
猜你喜欢
  • 1970-01-01
  • 2013-08-12
  • 1970-01-01
  • 1970-01-01
  • 2014-04-04
  • 1970-01-01
  • 2015-08-11
  • 2022-01-10
  • 2018-05-09
相关资源
最近更新 更多