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

2022-08-30 22:27:35

假设我们有两个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 类之前使用它?

谢谢

米歇尔


答案 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,我们看到最后一个参数(在此调用中为)称为 。这听起来很有趣。它用这个论点做了什么?1compile_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用户的角度)或动态上下文读取父类名和派生类名。然后,它尝试在类表中查找类条目(“ce”),如果未找到,则...它在编译时返回而不执行任何操作,但在运行时发出致命错误compile_time

这听起来非常重要。让我们回到 .如果未找到父类,该怎么办?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;

它似乎正在生成操作码,这些操作码将再次触发调用 - 但是这一次,的值将是(false)。do_bind_inherited_classcompile_time0

最后,class_exists PHP 函数的实现情况如何?查看源代码显示以下代码段:

found = zend_hash_find(EG(class_table), name, len+1, (void **) &ce);

伟大!此变量与我们之前看到的调用中涉及的变量相同!因此,的返回值取决于是否已将类的条目插入到 中。class_tableclass_tabledo_bind_inherited_classclass_existsclass_tabledo_bind_inherited_class

结论

Zend 编译器在编译时不对 include 指令执行操作(即使文件名是硬编码的)。

如果是这样,那么就没有理由基于未设置的标志发出类重新声明致命错误;该错误可能是无条件发出的。compile_time

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

从上面的最后一个代码片段中可以明显看出这一点,该代码片段设置了一个操作码以在执行脚本时注册类。此时,标志将和行为略有不同。ZEND_DECLARE_INHERITED_CLASS_DELAYEDcompile_timefalse

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

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

  • 其祖先都包含在同一源文件中的类在编译时注册;它们存在并且可以在该脚本中的任何点实例化
  • 在另一个源文件中定义了祖先的类在运行时注册;在虚拟机执行与源中的类定义相对应的操作码之前,这些类不存在于所有实际目的(返回,实例化给出致命错误)class_existsfalse

答案 2

这只与包含文件中的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 Aclass 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文档所说的内容中有所涉及,这可以被视为需要纠正的错误,但在主要时间,在调用或使用它们之前包括您的类


推荐