ser*_*gio 28 php inheritance php-internals
我研究类编译,它的序列和逻辑.
如果我在简单父级之前声明一个类:
class First extends Second{}
class Second{}
Run Code Online (Sandbox Code Playgroud)
这将工作正常.查看PHP版本的实例.
但是,如果父类还有一些尚未声明的父类(扩展或实现),如下例所示:
class First extends Second{}
class Second extends Third{}
class Third{}
Run Code Online (Sandbox Code Playgroud)
我会有一个错误:
致命错误:找不到"第二级"......
那么,为什么在第二个例子中它找不到Second
类?也许php无法编译这个类因为它还需要编译Third
类,还是什么?
我试图找出为什么在第一个例子中,PHP编译类第二,但如果它将有一些父类,它不会.我研究了很多,但没有确切的.
irc*_*ell 34
因此,PHP使用称为"后期绑定"的东西.基本上,继承和类定义直到文件编译结束才会发生.
有许多的原因.第一个是你展示的例子(first extends second {}
工作).第二个原因是opcache.
为了使编译在opcache领域正常工作,编译必须在没有来自其他编译文件的状态下进行.这意味着在编译文件时,类符号表将被清空.
然后,缓存该编译的结果.然后在运行时,当从内存加载编译的文件时,opcache运行后期绑定,然后执行继承并实际声明类.
class First {}
Run Code Online (Sandbox Code Playgroud)
当看到该类时,它会立即添加到符号表中.无论文件在哪里.因为不需要后期绑定任何东西,它已经完全定义.这种技术称为早期绑定,它允许您在声明之前使用类或函数.
class Third extends Second {}
Run Code Online (Sandbox Code Playgroud)
当看到它时,它被编译,但实际上没有声明.相反,它被添加到"后期绑定"列表中.
class Second extends First {}
Run Code Online (Sandbox Code Playgroud)
当最终看到它时,它也被编译,并没有实际声明.它被添加到后期绑定列表中,但之后 Third
.
所以现在,当后期绑定过程发生时,它会逐个浏览"后期绑定"类列表.它看到的第一个是Third
.然后它尝试找到Second
类,但不能(因为它实际上还没有声明).所以错误被抛出.
如果您重新安排课程:
class Second extends First {}
class Third extends Second {}
class First {}
Run Code Online (Sandbox Code Playgroud)
然后你会发现它运作正常.
好吧,PHP很有趣.让我们想象一系列文件:
<?php // a.php
class Foo extends Bar {}
<?php // b1.php
class Bar {
//impl 1
}
<?php // b2.php
class Bar {
//impl 2
}
Run Code Online (Sandbox Code Playgroud)
现在,Foo
您获得的最终实例将取决于您加载的是哪个b文件.如果你需要,b2.php
你会得到Foo extends Bar (impl2)
.如果你需要b1.php
,你会得到Foo extends Bar (impl1)
.
通常我们不会以这种方式编写代码,但有一些情况可能会发生.
在正常的PHP请求中,这很容易处理.原因是Bar
我们在编译时可以知道Foo
.所以我们可以相应地调整我们的编译过程.
但是,当我们将操作码缓存添加到混合中时,事情会变得复杂得多.如果我们Foo
用全局状态编译b1.php
,然后(在不同的请求中)切换到b2.php
,事情会以奇怪的方式破坏.
因此,在编译文件之前,操作码缓存会使全局状态为空.因此,a.php
它将被编译为好像它是应用程序中的唯一文件.
编译完成后,它会被缓存到内存中(以后的请求可以重用).
然后,在该点之后(或在将来的请求中从内存加载之后),发生"延迟"步骤.然后,它将编译的文件耦合到请求的状态.
这样,opcache可以更有效地将文件作为独立实体进行缓存,因为在读取缓存之后会发生与全局状态的绑定.
要了解原因,让我们来看看源代码.
在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;
}
Run Code Online (Sandbox Code Playgroud)
所以它最初发出一个操作码来声明继承的类.然后,在编译发生后,调用一个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))) {
if (CG(compiler_options) & ZEND_COMPILE_DELAYED_BINDING) {
uint32_t *opline_num = &CG(active_op_array)->early_binding;
while (*opline_num != (uint32_t)-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;
}
Run Code Online (Sandbox Code Playgroud)
外部if基本上尝试从符号表中获取类并检查它是否不存在.第二个if检查我们是否正在使用延迟绑定(opcache已启用).
然后,它复制操作码以将类声明为延迟的早期绑定数组.
最后,zend_do_delayed_early_binding()
调用该函数(通常由opcache),它循环遍历列表并实际绑定继承的类:
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;
}
Run Code Online (Sandbox Code Playgroud)
对于不扩展另一个类的类,顺序无关紧要.
任何正在扩展的类必须在它实现之前定义(或者必须使用自动加载器).