文件引入
在 PHP 中引入文件有两种方式,即 include 和 require,配对的还有 include_once 和 require_once。本节将详细介绍这两种方式的底层实现。以下面的 PHP 代码为例:
<?php
include "a.php";
require "c.php";
echo "require c.php";
php
关键字 include 和 require 生成的 Token 分别是 T_INCLUDE 和 T_REQUIRE。各自的语法规则同样定义在 zend_language_parser.y 文件中:
| T_INCLUDE expr
{ $$ = zend_ast_create_ex(ZEND_AST_INCLUDE_OR_EVAL, ZEND_INCLUDE, $2); }
| T_INCLUDE_ONCE expr
{ $$ = zend_ast_create_ex(ZEND_AST_INCLUDE_OR_EVAL, ZEND_INCLUDE_ONCE, $2); }
| T_REQUIRE expr
{ $$ = zend_ast_create_ex(ZEND_AST_INCLUDE_OR_EVAL, ZEND_REQUIRE, $2); }
| T_REQUIRE_ONCE expr
{ $$ = zend_ast_create_ex(ZEND_AST_INCLUDE_OR_EVAL, ZEND_REQUIRE_ONCE, $2); }
bash
从规则的定义中可以看出,在 PHP 中,关键字 include、include_once、require、require_once 生成的 AST 节点的 kind 均为 ZEND_AST_INCLUDE_OR_EVAL,并且该类型的节点只有一个孩子节点。子节点保存着引入文件的路径信息。通过 gdb 可以发现,生成的 opcodes 数组中的每条 opcode->handler
的函数名如下:
-
ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HANDLER;
-
ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HANDLER;
-
ZEND_ECHO_SPEC_CONST_HANDLER;
-
ZEND_RETURN_SPEC_CONST_HANDLER。
可以发现,两个关键字对应的 opcode 的 handler 函数均是 ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HANDLER。该函数解析文件的绝对路径并将该文件编译生成新的 op_array。如果新的 op_array 不为空,则重新执行一遍文件扫描、解析、编译、执行的过程。如果同个文件中有多条 include 语句和 require 语句,每条 include 语句和 require 语句都重新执行一遍这个流程。
include_once 和 require_once 会将解析出的文件路径加入到全局的 hashtable 表 EG(included_files) 中,下次再被引入时,会优先从 EG(included_files) 中查找。如果文件已经编译过,则不再对文件进行编译。在包含文件的错误处理上,PHP 支持两种模式:对于必要文件的错误,报错并终止(require 或 require_once);对于非必要文件的错误,抛出警告(include 或 include_once)。