由php中文文档一处错误引发的思考

php基础

浏览数:258

2020-6-26

PHP官方中文文档:

https://www.php.net/manual/zh…

类常量这一节

说类常量不能为数学运算的结果。但我业务代码中这样写是完全可行的

class MyEvent
{
    const READ = 1;
    
    const WRITE = 1 << 1;
    
    const ALL = self::READ | self::WRITE;
}

echo MyEvent::READ;
echo MyEvent::WRITE;
echo MyEvent::ALL;

我特意去看了源码,词法解析文件里面:

class_const_list:
        class_const_list ',' class_const_decl { $$ = zend_ast_list_add($1, $3); }
    |    class_const_decl { $$ = zend_ast_create_list(1, ZEND_AST_CLASS_CONST_DECL, $1); }
class_const_decl:
    identifier '=' expr backup_doc_comment { $$ = zend_ast_create(ZEND_AST_CONST_ELEM, $1, $3, ($4 ? zend_ast_create_zval_from_str($4) : NULL)); }
;
expr:
        variable
            { $$ = $1; }
    |    T_LIST '(' array_pair_list ')' '=' expr
            { $3->attr = ZEND_ARRAY_SYNTAX_LIST; $$ = zend_ast_create(ZEND_AST_ASSIGN, $3, $6); }
    |    '[' array_pair_list ']' '=' expr
            { $2->attr = ZEND_ARRAY_SYNTAX_SHORT; $$ = zend_ast_create(ZEND_AST_ASSIGN, $2, $5); }
    |    variable '=' expr
            { $$ = zend_ast_create(ZEND_AST_ASSIGN, $1, $3); }
    |    variable '=' '&' variable
            { $$ = zend_ast_create(ZEND_AST_ASSIGN_REF, $1, $4); }
    |    T_CLONE expr { $$ = zend_ast_create(ZEND_AST_CLONE, $2); }
    |    variable T_PLUS_EQUAL expr
            { $$ = zend_ast_create_assign_op(ZEND_ADD, $1, $3); }
    |    variable T_MINUS_EQUAL expr
            { $$ = zend_ast_create_assign_op(ZEND_SUB, $1, $3); }
    |    variable T_MUL_EQUAL expr
            { $$ = zend_ast_create_assign_op(ZEND_MUL, $1, $3); }
    |    variable T_POW_EQUAL expr
            { $$ = zend_ast_create_assign_op(ZEND_POW, $1, $3); }
    |    variable T_DIV_EQUAL expr
            { $$ = zend_ast_create_assign_op(ZEND_DIV, $1, $3); }
    |    variable T_CONCAT_EQUAL expr
            { $$ = zend_ast_create_assign_op(ZEND_CONCAT, $1, $3); }
    |    variable T_MOD_EQUAL expr
            { $$ = zend_ast_create_assign_op(ZEND_MOD, $1, $3); }
    |    variable T_AND_EQUAL expr
            { $$ = zend_ast_create_assign_op(ZEND_BW_AND, $1, $3); }
    |    variable T_OR_EQUAL expr
            { $$ = zend_ast_create_assign_op(ZEND_BW_OR, $1, $3); }
    |    variable T_XOR_EQUAL expr
            { $$ = zend_ast_create_assign_op(ZEND_BW_XOR, $1, $3); }
    |    variable T_SL_EQUAL expr
            { $$ = zend_ast_create_assign_op(ZEND_SL, $1, $3); }
    |    variable T_SR_EQUAL expr
            { $$ = zend_ast_create_assign_op(ZEND_SR, $1, $3); }
    |    variable T_COALESCE_EQUAL expr
            { $$ = zend_ast_create(ZEND_AST_ASSIGN_COALESCE, $1, $3); }
    |    variable T_INC { $$ = zend_ast_create(ZEND_AST_POST_INC, $1); }
    |    T_INC variable { $$ = zend_ast_create(ZEND_AST_PRE_INC, $2); }
    |    variable T_DEC { $$ = zend_ast_create(ZEND_AST_POST_DEC, $1); }
    |    T_DEC variable { $$ = zend_ast_create(ZEND_AST_PRE_DEC, $2); }
    |    expr T_BOOLEAN_OR expr
            { $$ = zend_ast_create(ZEND_AST_OR, $1, $3); }
    |    expr T_BOOLEAN_AND expr
            { $$ = zend_ast_create(ZEND_AST_AND, $1, $3); }
    |    expr T_LOGICAL_OR expr
            { $$ = zend_ast_create(ZEND_AST_OR, $1, $3); }
    |    expr T_LOGICAL_AND expr
            { $$ = zend_ast_create(ZEND_AST_AND, $1, $3); }
    |    expr T_LOGICAL_XOR expr
            { $$ = zend_ast_create_binary_op(ZEND_BOOL_XOR, $1, $3); }
    |    expr '|' expr    { $$ = zend_ast_create_binary_op(ZEND_BW_OR, $1, $3); }
    |    expr '&' expr    { $$ = zend_ast_create_binary_op(ZEND_BW_AND, $1, $3); }
    |    expr '^' expr    { $$ = zend_ast_create_binary_op(ZEND_BW_XOR, $1, $3); }
    |    expr '.' expr     { $$ = zend_ast_create_binary_op(ZEND_CONCAT, $1, $3); }
    |    expr '+' expr     { $$ = zend_ast_create_binary_op(ZEND_ADD, $1, $3); }
    |    expr '-' expr     { $$ = zend_ast_create_binary_op(ZEND_SUB, $1, $3); }
    |    expr '*' expr    { $$ = zend_ast_create_binary_op(ZEND_MUL, $1, $3); }
    |    expr T_POW expr    { $$ = zend_ast_create_binary_op(ZEND_POW, $1, $3); }
    |    expr '/' expr    { $$ = zend_ast_create_binary_op(ZEND_DIV, $1, $3); }
    |    expr '%' expr     { $$ = zend_ast_create_binary_op(ZEND_MOD, $1, $3); }
    |     expr T_SL expr    { $$ = zend_ast_create_binary_op(ZEND_SL, $1, $3); }
    |    expr T_SR expr    { $$ = zend_ast_create_binary_op(ZEND_SR, $1, $3); }
    |    '+' expr %prec '~' { $$ = zend_ast_create(ZEND_AST_UNARY_PLUS, $2); }
    |    '-' expr %prec '~' { $$ = zend_ast_create(ZEND_AST_UNARY_MINUS, $2); }
    |    '!' expr { $$ = zend_ast_create_ex(ZEND_AST_UNARY_OP, ZEND_BOOL_NOT, $2); }
    |    '~' expr { $$ = zend_ast_create_ex(ZEND_AST_UNARY_OP, ZEND_BW_NOT, $2); }
    |    expr T_IS_IDENTICAL expr
            { $$ = zend_ast_create_binary_op(ZEND_IS_IDENTICAL, $1, $3); }
    |    expr T_IS_NOT_IDENTICAL expr
            { $$ = zend_ast_create_binary_op(ZEND_IS_NOT_IDENTICAL, $1, $3); }
    |    expr T_IS_EQUAL expr
            { $$ = zend_ast_create_binary_op(ZEND_IS_EQUAL, $1, $3); }
    |    expr T_IS_NOT_EQUAL expr
            { $$ = zend_ast_create_binary_op(ZEND_IS_NOT_EQUAL, $1, $3); }
    |    expr '<' expr
            { $$ = zend_ast_create_binary_op(ZEND_IS_SMALLER, $1, $3); }
    |    expr T_IS_SMALLER_OR_EQUAL expr
            { $$ = zend_ast_create_binary_op(ZEND_IS_SMALLER_OR_EQUAL, $1, $3); }
    |    expr '>' expr
            { $$ = zend_ast_create(ZEND_AST_GREATER, $1, $3); }
    |    expr T_IS_GREATER_OR_EQUAL expr
            { $$ = zend_ast_create(ZEND_AST_GREATER_EQUAL, $1, $3); }
    |    expr T_SPACESHIP expr
            { $$ = zend_ast_create_binary_op(ZEND_SPACESHIP, $1, $3); }
    |    expr T_INSTANCEOF class_name_reference
            { $$ = zend_ast_create(ZEND_AST_INSTANCEOF, $1, $3); }
    |    '(' expr ')' {
            $$ = $2;
            if ($$->kind == ZEND_AST_CONDITIONAL) $$->attr = ZEND_PARENTHESIZED_CONDITIONAL;
        }
    |    new_expr { $$ = $1; }
    |    expr '?' expr ':' expr
            { $$ = zend_ast_create(ZEND_AST_CONDITIONAL, $1, $3, $5); }
    |    expr '?' ':' expr
            { $$ = zend_ast_create(ZEND_AST_CONDITIONAL, $1, NULL, $4); }
    |    expr T_COALESCE expr
            { $$ = zend_ast_create(ZEND_AST_COALESCE, $1, $3); }
    |    internal_functions_in_yacc { $$ = $1; }
    |    T_INT_CAST expr        { $$ = zend_ast_create_cast(IS_LONG, $2); }
    |    T_DOUBLE_CAST expr    { $$ = zend_ast_create_cast(IS_DOUBLE, $2); }
    |    T_STRING_CAST expr    { $$ = zend_ast_create_cast(IS_STRING, $2); }
    |    T_ARRAY_CAST expr    { $$ = zend_ast_create_cast(IS_ARRAY, $2); }
    |    T_OBJECT_CAST expr    { $$ = zend_ast_create_cast(IS_OBJECT, $2); }
    |    T_BOOL_CAST expr    { $$ = zend_ast_create_cast(_IS_BOOL, $2); }
    |    T_UNSET_CAST expr    { $$ = zend_ast_create_cast(IS_NULL, $2); }
    |    T_EXIT exit_expr    { $$ = zend_ast_create(ZEND_AST_EXIT, $2); }
    |    '@' expr            { $$ = zend_ast_create(ZEND_AST_SILENCE, $2); }
    |    scalar { $$ = $1; }
    |    '`' backticks_expr '`' { $$ = zend_ast_create(ZEND_AST_SHELL_EXEC, $2); }
    |    T_PRINT expr { $$ = zend_ast_create(ZEND_AST_PRINT, $2); }
    |    T_YIELD { $$ = zend_ast_create(ZEND_AST_YIELD, NULL, NULL); CG(extra_fn_flags) |= ZEND_ACC_GENERATOR; }
    |    T_YIELD expr { $$ = zend_ast_create(ZEND_AST_YIELD, $2, NULL); CG(extra_fn_flags) |= ZEND_ACC_GENERATOR; }
    |    T_YIELD expr T_DOUBLE_ARROW expr { $$ = zend_ast_create(ZEND_AST_YIELD, $4, $2); CG(extra_fn_flags) |= ZEND_ACC_GENERATOR; }
    |    T_YIELD_FROM expr { $$ = zend_ast_create(ZEND_AST_YIELD_FROM, $2); CG(extra_fn_flags) |= ZEND_ACC_GENERATOR; }
    |    inline_function { $$ = $1; }
    |    T_STATIC inline_function { $$ = $2; ((zend_ast_decl *) $$)->flags |= ZEND_ACC_STATIC; }
;

很明显,只要符合expr的定义,在词法解析阶段就不会有问题。

class Test
{
    const A = 1 + 1;
}

这样写是完全可行的,但有的人就会问了,为什么我这样写是不行的呢?

class Test
{
    const A = Test2::$a + 1;
}

class Test2
{
    public static $a = 1;
}

你不是说只要符合expr里的规则,词法解析阶段就没有问题吗?
const A = Test2::$a + 1;符合规则 expr '+' expr啊,为什么报错了。

别急,少年,先看看你的报错是啥?

PHP Fatal error:  Constant expression contains invalid operations in xxxx

看到没有,是Fatal error,而不是Parse error,所以词法解析阶段是没有问题的,问题出在哪里?编译阶段出问题了,看以下代码:

void zend_compile_const_expr(zend_ast **ast_ptr) /* {{{ */
{
    zend_ast *ast = *ast_ptr;
    if (ast == NULL || ast->kind == ZEND_AST_ZVAL) {
        return;
    }

    if (!zend_is_allowed_in_const_expr(ast->kind)) {
        zend_error_noreturn(E_COMPILE_ERROR, "Constant expression contains invalid operations");
    }

    switch (ast->kind) {
        case ZEND_AST_CLASS_CONST:
            zend_compile_const_expr_class_const(ast_ptr);
            break;
        case ZEND_AST_CLASS_NAME:
            zend_compile_const_expr_class_name(ast_ptr);
            break;
        case ZEND_AST_CONST:
            zend_compile_const_expr_const(ast_ptr);
            break;
        case ZEND_AST_MAGIC_CONST:
            zend_compile_const_expr_magic_const(ast_ptr);
            break;
        default:
            zend_ast_apply(ast, zend_compile_const_expr);
            break;
    }
}

看到触发这条错误信息的判断条件了吗?zend_is_allowed_in_const_expr(ast->kind),如果抽象语法树的类型不在允许范围内,就算符合词法解析规则,依然会报错。
看看zend_is_allowed_in_const_expr的实现.

zend_bool zend_is_allowed_in_const_expr(zend_ast_kind kind) /* {{{ */
{
    return kind == ZEND_AST_ZVAL || kind == ZEND_AST_BINARY_OP
        || kind == ZEND_AST_GREATER || kind == ZEND_AST_GREATER_EQUAL
        || kind == ZEND_AST_AND || kind == ZEND_AST_OR
        || kind == ZEND_AST_UNARY_OP
        || kind == ZEND_AST_UNARY_PLUS || kind == ZEND_AST_UNARY_MINUS
        || kind == ZEND_AST_CONDITIONAL || kind == ZEND_AST_DIM
        || kind == ZEND_AST_ARRAY || kind == ZEND_AST_ARRAY_ELEM
        || kind == ZEND_AST_UNPACK
        || kind == ZEND_AST_CONST || kind == ZEND_AST_CLASS_CONST
        || kind == ZEND_AST_CLASS_NAME
        || kind == ZEND_AST_MAGIC_CONST || kind == ZEND_AST_COALESCE;
}

清楚了,刚才的例子之所以通不过,是因为类的静态变量ast->kind为ZEND_AST_STATIC_PROP,并不在上面范围里面,所以通不过。如果你把Test2中的静态变量换成常量,编译就可以通过,因为ZEND_AST_CLASS_CONST是在允许范围内的。

所以官方中文文档中的这句话应该改为:
常量的值必须是一个常量表达式,不能是变量、类属性或函数调用

作者:church