循环语句
循环语句通常由条件表达式和普通表达式构成。在 PHP 中,循环语句有 foreach、while、for、do while,下面分别介绍。
foreach语句
foreach 是 PHP 提供的遍历数组或对象的方式,语法简洁明了:
<?php
foreach(expression as key => value ){
statement
}
//还有一种写法 foreach(expression as value )
foreach 结构由 3 部分组成,即 expression(可以是数组或者对象)和 foreach 变量 key、value。这里不论是数组还是对象,本质上是对 HashTable 进行遍历,因为 PHP 7 中的数组实现就是 HashTable,而遍历对象实际上是对对象的属性进行遍历。
foreach 对应的 Token 是 T_FOREACH, as 对应的 Token 为 T_AS。同样,根据 Token 查看其语法规则:
| T_FOREACH '(' expr T_AS foreach_variable ')' foreach_statement
{ $$ = zend_ast_create(ZEND_AST_FOREACH, $3, $5, NULL, $7); }
| T_FOREACH '(' expr T_AS foreach_variable T_DOUBLE_ARROW foreach_variable ')'
foreach_statement
{ $$ = zend_ast_create(ZEND_AST_FOREACH, $3, $7, $5, $9); }
可以看出,foreach 关键字会创建一个 kind 为 ZEND_AST_FOREACH 的节点。以该节点为根,共有 4 个孩子,分别对应为 expression(需要遍历的变量)、variable1(foreach 变量 key)、variable(foreach 变量 value)和 statement(普通表达式)。生成的 AST 结构如图12-3所示。

节点存在的顺序是固定的。如果 foreach 中不需要 key,生成的 AST 中的 child1 节点为空。 |
foreach 遍历的过程是对变量 key 和 value 不断赋值,并且记录当前遍历位置的过程。可能读者已经猜到,与条件判断语句类似,foreach 语法的关键点仍然是跳转——如何在一次遍历之后,跳转到下次遍历要开始的位置。编译的过程大致如下:
-
编译 expression(对应图12-3中根节点的 child0)生成复制数组或对象的 opcode FE_RESET_R。这里如果 value 是引用类型,会生成 opcode ZEND_FE_RESET_RW。如果数组或者对象为空,opcode 还需要记录需要跳过的 opcode 数。
-
编译 value(对应图12-3中根节点的 child2)生成 opcode 为 FE_FETCH_R 的操作。如果 value 是引用类型,同样会生成 opcode ZEND_FE_FETCH_RW。此时 opcode 还需要记录遍历指针到达末尾需要跳过的 opcode 数。
-
如果存在 key(对应图12-3中根节点的 child1)则编译一条 ASSIGN,对 key 进行赋值操作。
-
编译循环体中的普通表达式列表 statement。
-
生成一条 ZEND_JMP。因为一次循环结束后,需要跳回遍历开始的位置,然后重新走流程。
-
全部 opcode 条数已经确定,开始设置步骤 1)和步骤 2)中需要跳过的 opcode 数。编译 foreach 的函数是 zend_compile_foreach,位于 zend/zend_compile.c 文件中,主要流程如下:
void zend_compile_foreach(zend_AST *AST) /* {{{ */
{
if (by_ref) {
value_AST = value_AST->child[0];
}
if (by_ref && is_variable) {
zend_compile_var(&expr_node, expr_AST, BP_VAR_W);
} else {
zend_compile_expr(&expr_node, expr_AST);
}
/*如果是引用会编译一条ZEND_SEPARATE opcode*/
if (by_ref) {
zend_separate_if_call_and_write(&expr_node, expr_AST, BP_VAR_W);
}
opnum_reset = get_next_op_number(CG(active_op_array));
opline = zend_emit_op(&reset_node, by_ref ? ZEND_FE_RESET_RW : ZEND_FE_RESET_
R, &expr_node, NULL);
zend_begin_loop(ZEND_FE_FREE, &reset_node);
opnum_fetch = get_next_op_number(CG(active_op_array));
// 编译生成FE_FETCH_R opcode
opline = zend_emit_op(NULL, by_ref ? ZEND_FE_FETCH_RW : ZEND_FE_FETCH_R,
&reset_node, NULL);
if (key_AST) {
opline = &CG(active_op_array)->opcodes[opnum_fetch];
zend_make_tmp_result(&key_node, opline);
/*编译一条ASSIGN对key赋值*/
zend_emit_assign_znode(key_AST, &key_node);
}
/*编译循环体里面的普通表达式列表*/
zend_compile_stmt(stmt_AST);
/*生成zend_jmp opcode,在一次循环结束后跳到遍历开始位置*/
zend_emit_jump(opnum_fetch);
/*设置跳过的opcode数*/
opline = &CG(active_op_array)->opcodes[opnum_reset];
opline->op2.opline_num = get_next_op_number(CG(active_op_array));
opline = &CG(active_op_array)->opcodes[opnum_fetch];
opline->extended_value = get_next_op_number(CG(active_op_array));
zend_end_loop(opnum_fetch, &reset_node);
opline = zend_emit_op(NULL, ZEND_FE_FREE, &reset_node, NULL);
}
关于 opcode 对应的 handler,这里不再赘述。
Zend 引擎在执行时,首先重置 expression 中遍历的位置,这时如果发现遍历的数组或者对象(expression)为空,跳过循环体 statement,直接跳转到结束的位置;反之对 key 和 value 进行赋值,开始执行循环体 statement 部分。循环体 statement 执行完再执行 ZEND_JMP,跳到循环体开始的位置,开始下次遍历,最终达到遍历数组或者对象中各个元素的目的。其执行流程如图12-4所示。

while语句
while 语句也是一种常用的循环语句,关键字 while 生成的 Token 是 T_WHILE。while 语法规则相对简单:
| T_WHILE '(' expr ')' while_statement
{ $$ = zend_ast_create(ZEND_AST_WHILE, $3, $5); }
while_statement:
statement { $$ = $1; }
| ':' inner_statement_list T_ENDWHILE '; ' { $$ = $2; }
;
while 语法会创建一个 kind 为 ZEND_AST_WHILE 的 AST 节点。以其为根,有两个孩子节点。child0 用来记录条件表达式(condition), child1 节点存储普通循环体 statement。AST 示意图如图12-5所示。
condition 语句一般会生成 kind 为 ZEND_AST_BINARY_OP 节点的子树。该节点在编译时会生成临时变量(true 或 false)。引擎根据临时变量结果,确定下一个要执行的 opcode 的位置。如果临时变量结果为 true,则执行 while 循环体的 statement;否则,跳出循环。这里与跳转逻辑相似。
condition 不同,生成的子节点的 kind 也是不同的。读者可以自行尝试,观察变化。 |
while 语法的编译过程大致如下。
-
编译条件表达式(condition)部分,同时,需要生成一条 ZEND_JMP 的 opcode,跳转到条件表达式(condition)生成的 opcode 的位置。目前还不能确定下一个 opcode 的位置(jmp_offset),需要在编译完循环体 statement 后更新。
-
编译循环体 statement,编译后会更新步骤 1)中 jmp_offset 的值。
-
编译条件表达式 condition。
-
编译生成 ZEND_JMPNZ 的 opcode,如果条件成立跳到循环体开始的位置,否则继续执行。从上面的 AST 可以看出,while 的语法相对简单,因此它的编译过程也相对简单,编译函数是 zend_compile_while,同样定义在 zend/zend_compile.c 文件中。为了方便读者理解,这里只展示函数的主要流程,具体如下:
void zend_compile_while(zend_AST *AST) /* {{{ */
{
opnum_jmp = zend_emit_jump(0);
zend_begin_loop(ZEND_NOP, NULL);
opnum_start = get_next_op_number(CG(active_op_array));
zend_compile_stmt(stmt_AST); /*编译条件表达式*/
opnum_cond = get_next_op_number(CG(active_op_array));
zend_update_jump_target(opnum_jmp, opnum_cond); /*更新zend_jmp跳的位置*/
zend_compile_expr(&cond_node, cond_AST); /*编译条件表达式*/
/*编译ZEND_JMPNZ */
zend_emit_cond_jump(ZEND_JMPNZ, &cond_node, opnum_start);
zend_end_loop(opnum_cond, NULL);
}
关于 while 语句的执行跳转逻辑,这里不再详述。
for语句
for 语句的 PHP 代码示例如下:
<?php
for( expressions1 ; expressions2 ; expressions3 ) {
statement
}
for 语句由两大部分组成,即多个表达式 expressions 和循环体 statement。for 关键字生成的 Token 为 T_FOR,它的语法规则如下:
T_FOR '(' for_exprs '; ' for_exprs '; ' for_exprs ')' for_statement
{ $$ = zend_ast_create(ZEND_AST_FOR, $3, $5, $7, $9); }
for_statement:
statement { $$ = $1; }
':' inner_statement_list T_ENDFOR '; ' { $$ = $2; }
for 语法结构生成的 AST 根节点是 kind 为 ZEND_AST_FOR 的节点。其有 4 个子节点,其中前三个节点都是表达式列表(expressions),第四个节点保存循环体 statement 信息。这里值得注意的是,这 4 个节点都是 zend_ast_list 类型,意味着它们都支持多个表达式。本书第 10 章介绍过 AST 中 list 节点和普通节点的区别,读者可以回读了解。for 语法结构生成的 AST 示意图如图12-6所示。

Zend 引擎编译 for 语法结构从 ZEND_AST_FOR 节点的 child0 开始。这里需要注意的是,前三个子节点虽都是条件表达式,却有所区别。child0 用来初始化,child1 用来条件判断,child2 用来做循环控制。编译过程大致如下:
-
编译 child0 对应的表达式,生成 ZEND_ASSGIN 的 opcode,该过程会生成临时变量。
-
生成 ZEND_JMP 的 opcode,用来跳转到条件判断的位置(编译过程可以看出为何初始化之后不是条件判断的opcode)。当前阶段不能确定跳转的位置,需要后续更新。
-
编译循环体。
-
编译 child2 对应的表达式 expressions。
-
编译 child1 对应的表达式列表 expressions, for 语句的判断条件在这里编译,并更新步骤 2)中 jmp_offset 的值。
-
生成 ZEND_JMPNZ。依据步骤 6)中条件判断的结果,决定下一条要执行的 opcode 的位置。for 语法结构的编译实现过程简化如下:
void zend_compile_for(zend_AST *AST) /* {{{ */
{
//编译第一个for_exprs
zend_compile_expr_list(&result, init_AST);
zend_do_free(&result);
/*生成ZEND_JMP*/
opnum_jmp = zend_emit_jump(0);
zend_begin_loop(ZEND_NOP, NULL);
/*编译循环体*/
opnum_start = get_next_op_number(CG(active_op_array));
zend_compile_stmt(stmt_AST);
opnum_loop = get_next_op_number(CG(active_op_array));
/*编译第三个for_exprs*/
zend_compile_expr_list(&result, loop_AST);
zend_do_free(&result);
/*更新zend_jmp需要跳过的opcode数*/
zend_update_jump_target_to_next(opnum_jmp);
/*编译第二个for_exprs*/
zend_compile_expr_list(&result, cond_AST);
zend_do_extended_info();
/*生成ZEND_JMPNZ */
zend_emit_cond_jump(ZEND_JMPNZ, &result, opnum_start);
zend_end_loop(opnum_loop, NULL);
}
for 语法的执行过程类似 while 语法的执行过程,首先通过 ZEND_JMP 跳到条件判断的 opcode 位置,如果条件返回 true 则跳转到循环体开始的位置,这里不再赘述。for 语句执行过程如图12-7所示。

do while语句
do while 与 while 基本一样,不同的是 do while 是先执行循环体,再判断 while 条件表达式,决定是否继续执行循环体。根据 while 的实现,不难想象 do while 的基本流程。以如下 PHP 代码为例:
<?php
do{
//$i++; echo $i;
statement
}while( condition ); //$i < 10
我们直接看下它生成的 AST,与 while 语法生成的 AST 做下对比。
在实际编译的 PHP 代码中以注释为例,这里为了便于理解用 condition 代替条件,用 statement 代替循环体。 |
对比图12-5中 while 结构生成的 AST,可以发现 do while 结构中循环体 statement 生成的节点在左边 child0 位置,而条件 condition 生成的节点在右边 child1 位置。相同的是这两棵树下面只有两个节点。do while 语法编译的过程跟 while 语法的流程基本一样,这里不再展开介绍。

do while 的执行示意图如图12-9所示。
