编译器代码表明这是设计,虽然我不知道这背后的官方推理。我也不知道需要多少努力可靠地实现这个功能,但是在目前做的事情的方式肯定有一些限制。
虽然我对PHP编译器的了解并不广泛,但我将尝试说明我相信的情况,以便您可以看到哪里有问题。您的代码示例是此过程的良好候选对象,因此我们将使用:
class Foo {
public $path = array(
realpath(".")
);
}
你很清楚,这会导致语法错误。这是PHP grammar的结果,这使得以下相关定义:
class_variable_declaration:
//...
| T_VARIABLE '=' static_scalar //...
;
因此,当定义变量的值如$ path时,期望值必须匹配静态标量的定义。不出所料,这是一个误导,因为静态标量的定义也包括其值也是静态标量的数组类型:
static_scalar: /* compile-time evaluated scalars */
//...
| T_ARRAY '(' static_array_pair_list ')' // ...
//...
;
让我们假设语法不同,并且类变量delcaration规则中的注释行看起来更像下面这样,它将匹配您的代码示例(尽管中断了其他有效的分配):
class_variable_declaration:
//...
| T_VARIABLE '=' T_ARRAY '(' array_pair_list ')' // ...
;
重新编译PHP后,示例脚本将不再失败,并显示该语法错误。相反,它会失败,编译时错误“无效的绑定类型”。由于代码现在基于语法是有效的,这表明在编译器的设计中实际上存在导致麻烦的特定事物。为了弄清楚这是什么,让我们回到原来的语法一会儿,想象代码示例有一个有效的$ path = array(2);。
使用语法作为指南,可以通过在解析此代码示例时在compiler code中调用的操作。我留下了一些不太重要的部分,但过程看起来像这样:
// ...
// Begins the class declaration
zend_do_begin_class_declaration(znode, "Foo", znode);
// Set some modifiers on the current znode...
// ...
// Create the array
array_init(znode);
// Add the value we specified
zend_do_add_static_array_element(znode, NULL, 2);
// Declare the property as a member of the class
zend_do_declare_property('$path', znode);
// End the class declaration
zend_do_end_class_declaration(znode, "Foo");
// ...
zend_do_early_binding();
// ...
zend_do_end_compilation();
虽然编译器在这些各种方法中做了很多,但重要的是要注意几个事情。
>对zend_do_begin_class_declaration()的调用导致对get_next_op()的调用。这意味着它向当前操作码数组添加一个新的操作码。
> array_init()和zend_do_add_static_array_element()不生成新的操作码。而是立即创建数组并将其添加到当前类的属性表中。方法声明以类似的方式工作,通过zend_do_begin_function_declaration()中的特殊情况。
> zend_do_early_binding()使用当前操作码数组上的最后一个操作码,在将其设置为NOP之前检查以下类型之一:
> ZEND_DECLARE_FUNCTION
> ZEND_DECLARE_CLASS
> ZEND_DECLARE_INHERITED_CLASS
> ZEND_VERIFY_ABSTRACT_CLASS
> ZEND_ADD_INTERFACE
注意,在最后一种情况下,如果操作码类型不是预期类型之一,则会抛出错误 – “无效的绑定类型”错误。从这里,我们可以告诉,允许非静态值以某种方式分配导致最后一个操作码是不是预期的。那么,当我们使用修改语法的非静态数组时会发生什么?
而不是调用array_init(),编译器准备参数和调用zend_do_init_array()。这反过来调用get_next_op()并添加一个新的INIT_ARRAY opcode,产生如下:
DECLARE_CLASS 'Foo'
SEND_VAL '.'
DO_FCALL 'realpath'
INIT_ARRAY
这里是问题的根源。通过添加这些操作码,zend_do_early_binding()获取意外的输入并抛出异常。由于早期绑定类和函数定义的过程似乎相当完整的PHP编译过程,它不能只是被忽略(虽然DECLARE_CLASS生产/消费是一种混乱)。同样,尝试并评估这些附加操作码(不能确定给定的函数或类已经解析)是不现实的,因此没有办法避免生成操作码。
一个潜在的解决方案是构建一个范围限于类变量声明的新操作码数组,类似于如何处理方法定义。这样做的问题是决定何时评估这样的运行一次序列。当包含该类的文件被加载时,首次访问该属性时,或者当构造该类型的对象时,它会做什么?
正如你所指出的,其他动态语言已经找到了一种处理这种情况的方法,因此做出决定并使其工作是不可能的。从我可以告诉的是,在PHP的情况下这样做不会是一个单线的修复,语言设计师似乎已经决定这不是值得包括在这一点上的东西。