因此,PHP使用了所谓的“延迟绑定”。基本上,继承和类定义直到文件编译结束才发生。
这其中有许多原因。第一个原因是你所示例的:(first extends second {}
起作用)。第二个原因是 opcache。
为了使编译在 opcache 的领域内正确工作,编译必须在没有其他已编译文件状态的情况下进行。这意味着,在编译文件时,类符号表被清空。
然后,将缓存该编译结果。然后在运行时,当从内存加载已编译文件时,opcache 运行延迟绑定,然后进行继承并实际声明类。
class First {}
当那个类被看到时,它立即被添加到符号表中。不管它在文件中的位置在哪里。因为没有延迟绑定任何东西,它已经完全定义了。这种技术称为“早期绑定”,它允许您在声明之前使用类或函数。
class Third extends Second {}
当这个变量被看到时,它已经被编译了,但实际上还没有被声明。相反,它会被添加到一个“延迟绑定”的列表中。
class Second extends First {}
当最终看到这个时,它也被编译了,而不是实际声明。它被添加到后期绑定列表中,但在 Third
后面。
因此,现在当进行后期绑定过程时,它会逐一检查“后期绑定”的类列表。它首先看到的是 Third
。然后尝试找到 Second
类,但找不到(因为它实际上还没有被声明)。所以会抛出错误。
如果重新排列这些类:
class Second extends First {}
class Third extends Second {}
class First {}
那么你会看到它能够正常工作。
为什么要这样做???
嗯,PHP很有趣。我们来想象一系列的文件:
<?php
class Foo extends Bar {}
<?php
class Bar {
}
<?php
class Bar {
}
现在,你获取的Foo
实例将取决于你加载了哪个b文件。如果你加载了b2.php
,你将得到Foo extends Bar(impl2)
。如果你加载了b1.php
,你将得到Foo extends Bar(impl1)
。
通常我们不会这样编写代码,但有几种情况可能会出现。
在正常的PHP请求中,处理这个问题非常简单。原因是我们在编译Foo
时可以了解Bar
。因此,我们可以相应地调整编译过程。
但是,当我们将opcode缓存加入其中时,事情变得更加复杂。如果我们使用b1.php
的全局状态来编译Foo
,然后在以后(在不同的请求中)切换到b2.php
,事情会以奇怪的方式崩溃。
因此,opcode缓存在编译文件之前将全局状态置为空。因此,a.php
将被编译为应用程序中唯一的文件。
编译完成后,它将被缓存在内存中(以便供以后的请求重复使用)。
然后,在那一点之后(或在以后的请求中从内存中加载之后),"延迟"步骤发生。这样,编译的文件就与请求的状态耦合在一起。
这样,opcode缓存可以更有效地将文件作为独立实体缓存,因为与全局状态的绑定是在缓存从中读取后发生的。
源代码。
为了理解原因,让我们看看源代码。
在Zend/zend_compile.c中,我们可以看到编译类的函数:zend_compile_class_decl()
。大约在一半的位置,你会看到以下代码:
if (extends_ast) {
opline->opcode = ZEND_DECLARE_INHERITED_CLASS;
opline->extended_value = extends_node.u.op.var;
} else {
opline->opcode = ZEND_DECLARE_CLASS;
}
它最初会发出一个操作码来声明继承类。编译完成后,将调用名为zend_do_early_binding()
的函数。该函数预先声明文件中的函数和类(因此它们在顶部可用)。对于普通类和函数,它只是将它们添加到符号表中(声明它们)。
有趣的部分是继承的情况:
if (((ce = zend_lookup_class_ex(Z_STR_P(parent_name), parent_name + 1, 0)) == NULL) ||
((CG(compiler_options) & ZEND_COMPILE_IGNORE_INTERNAL_CLASSES) &&
(ce->type == ZEND_INTERNAL_CLASS)))
*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语句主要是试图从符号表中获取类并检查是否不存在。第二个if语句检查我们是否正在使用延迟绑定(启用了opcache)。
然后,它将用于声明类的操作码复制到延迟早期绑定数组中。
最后,通常由opcache调用函数zend_do_delayed_early_binding()
,该函数遍历列表并实际绑定继承的类:
while (opline_num != (uint32_t)-1) {
zval *parent_name = RT_CONSTANT(op_array, op_array->opcodes[opline_num-1].op2);
if ((ce = zend_lookup_class_ex(Z_STR_P(parent_name), parent_name + 1, 0)) != NULL) {
do_bind_inherited_class(op_array, &op_array->opcodes[opline_num], EG(class_table), ce, 0);
}
opline_num = op_array->opcodes[opline_num].result.opline_num;
}
简而言之
对于没有继承其他类的类,顺序并不重要。
任何被继承的类,在实现之前必须先定义(或者使用自动加载器)。
Second
应该扩展First
,Third
则应该扩展Second
。至少在通常情况下是这样做的。 - Sverri M. Olsen