Press "Enter" to skip to content

深入理解PHP之require/include顺序

在大型的Web项目中, include_path是一个模块化设计的根本中的根本(当然,现在也有很多基于autoload的设计, 这个不影响本文的探讨), 但是正是因为include_path, 经常会让我们遇到一些因为没有找到正确的文件而导致的看似"诡异"的问题.
也就有了如下的疑问:
include_path是怎么起作用的?
如果有多个include_path顺序是怎么样的?
什么情况下include_path不起作用?
今天, 我就全面的介绍下这个问题, 先从一个例子开始吧.
如下的目录结构:

  root
    ├ 1.php
    ├ 3.php
    └ subdir
    	├ 2.php
		└ 3.php

在1.php中:

<?php
ini_set("include_path", ".:path_to_subdir");
require("2.php");
?>

而在2.php中:

<?php
require("3.php");
?>

而在root目录下的3.php打印出"root", 在subdir目录下的3.php打印出"subdir";
现在, 我的问题来了:
1. 当在root目录下运行1.php, 会得到什么输出?
2. 在subdir下运行上一级目录的1.php, 有会得到什么输出?
3. 当取消include_path中的当前目录path(也就是include_path="path_to_subdir"), 上面俩个问题又会是什么输出?
PHP中的include_path
PHP在遇到require(_once)/include(_once)的指令的时候, 首先会做如下的判断:

要包含的文件路径是绝对路径么?
 如果是, 则直接包含, 并结束.
 如果不是, 进入另外的逻辑(经过多次调用, 宏展开后进入_php_stream_fopen_with_path)寻找此文件.

接下来, 在_php_stream_fopen_with_path中, 会做如下判断:

要包含的文件路径是相对路径么(形如./file, ../dir/file, 以下用"目录相对路径代替")?
 如果是, 则跳过include_path的作用逻辑, 直接解析相对路径(随后单独介绍).

会根据include_path,和当前执行文件的path组成一个待选的目录列表, 比如对于文章前面的例子来说, 会形成一个如下的待选列表

".:path_to_subdir:current_script_dir"

然后, 依次从待选列表头部开始, 根据DEFAULT_DIR_SEPARATOR(本文的环境是":")取出待选列表中的一个路径, 然后把要包含的文件名附加在这个路径后面, 进行尝试. 如果成功包含, 则返回, 否则继续下一个待选路径.
到现在为止, 我们已经可以回答我开头提出的3个问题了.
1. 因为在root目录下执行, 所以在1.php中包含2.php的时候, include_path的第二个待选路径起了作用(path_to_subdir), 找到了path_to_subdir/2.php, 而在2.php包含3.php的时候, 当前工作目录是root下, 所以在包含3.php的时候, include_path的第一个待选路径"."(当前工作目录)下就找到的匹配的文件, 所以得到的输出是"root".
2. 同1, 只不过当前的路径是subdir, 所以得到的输出是"subdir".
3. 因为没有了当前路径为include_path, 所以在root目录下运行的时候2.php中包含3.php的时候, 是path_to_subdir起了作用, 所以无论在root还是subdir都将得到"subdir"的输出.
而如果在2.php中清空include_path,

<?php
ini_set("include_path", '');
require("3.php");
?>

那么将会是current_script_dir起作用, 而这个时候current_script_dir是2.php的路径, 所以还是会得到"subdir"的输出.
目录相对路径
在使用目录相对路径的情况下, 相对路径的基点, 永远都是当前工作目录.
为了说明在目录相对路径下的情况,我们再看个列子, 还是上面的目录结构, 只不过1.php变成了:

<?php
ini_set("include_path", "/");
require("./subdir/2.php");
?>

2.php变成了:

<?php
require("./3.php");
?>

如果在root目录下执行, 2.php中寻找3.php将会在当前目录的相对路径下寻找, 所以得到的输出是"root", 而如果是在subdir下执行上一级目录的1.php(php -f ../1.php), 将会因为在subdir下找不到"./subdir/2.php"而异常退出.
后记
1. 因为使用include_path和相对路径的情况下, 性能会和寻找的次数有关, 最坏的情况下, 如果你有10个include_path, 那么最多可能会重试11次才能找到要包含的文件, 所以, 在能使用绝对路径的情况下最好使用绝对路径.
2. 因为目录相对路径的basedir, 永远都是当前工作路径, 如果要使用, 需要和实际部署路径相关, 所以实际使用的很少(当然, 也有借助chdir来完成的模块).
3. 在模块化的系统设计中, 一般应该在模块内, 通过获取模块的部署路径(dirname(__FILE__), php5.3以后更是提供了__DIR__常量)从而使用绝对路径.

37 Comments

  1. qwe
    qwe December 7, 2019

    有点没看懂,

  2. business coaching training
    business coaching training October 1, 2014

    I think everything posted was actually very reasonable.
    However, what about this? what if you were to create a awesome post title?
    I mean, I don’t want to tell you how to run your website, however what if you added
    a headline that grabbed folk’s attention? I mean 深入理解PHP之require/include顺序 | 风雪之隅 is
    kinda boring. You ought to look at Yahoo’s front page and note how they create post
    titles to get viewers interested. You might add a video or a related picture or two to get people excited about everything’ve written. Just my opinion, it might make your website a
    little livelier.

  3. Berc
    Berc October 10, 2011

    我最近遇到一个很奇怪的问题
    例如 1.php 中 include ‘common/2.php’;
    1.php和目录处于同一级目录。
    如上写,在我测试的两台电脑都可以用,这应该是由于include_path=’.;c:\php52′ 的作用。
    但是我这样写 include ‘./common/2.php’;就无法找到文件2.php了(两台电脑,一台可以,另外一台就不行)
    根据手册说的如果以. 或..开头,会直接在当前目录开始查找,为什么其中一台电脑就不行了呢?
    希望鸟哥看一看这个奇怪的问题。
    如果可以的话,能否发到我邮箱 308840239#qq.com
    再次先谢过了

  4. 雪候鸟
    雪候鸟 March 9, 2011

    @liujin834 你是想做什么呢? 应用场景, 还是只是说对这段代码本身的改进?

  5. liujin834
    liujin834 March 9, 2011

    您好!我想请教一个问题,我现在的代码是这样写的
    if ($_SERVER[“DOCUMENT_ROOT”]) {
    $Root = $_SERVER[“DOCUMENT_ROOT”];
    $Root = rtrim($Root,”/\\”);
    }
    else {
    $Root = str_replace(DIRECTORY_SEPARATOR . “include”, “”, dirname(__FILE__));
    }
    define(“ROOT”, $Root);
    $includePaths = array(
    ROOT . “/libs”,
    get_include_path()
    );
    set_include_path(implode(PATH_SEPARATOR, $includePaths));
    怎么改进才能使效率更高一些?

  6. 小兴
    小兴 February 16, 2011

    我做了个简单的测试,有两个页面a.php和b.php,并且a.php中require了b.php, 代码如下:
    a.php
    test();
    require ‘b.php’;
    function test()
    {
    echo ‘a.php test’;
    }
    ===========================================
    b.php
    function test()
    {
    //重复定义函数
    }
    程序运行结果是:
    a.php test
    并且抛出一个函数重复定义的致命错误,我的疑问是:
    1. a.php页面在b.php页面抛出了一个致命错误的前提下为什么还能正确输出本页面的test函数结果?
    2. 包含页面和被包含页面之间的编译、执行顺序的原理是怎么样的?
    非常希望能得到你的解惑,谢谢!

  7. 小兴
    小兴 February 9, 2011

    非常抱歉,第三点是我遗漏了代码,在增加了top函数后,a.php页面还是可以正常运行test和top函数的,假设应该是成立的
    “3. 从运行的结果来看,本页的函数test应该是在执行包含文件操作之前已经被预处理过了,但是我在a.php和b.php页面中都增加了一个函数top,代码如下:
    function top()
    {
    echo ‘top’;
    }
    这个时候a.php页面还是只能运行test函数的结果,不管top函数是在b.php页面中test函数定义之前还是之后,a.php都无法运行top函数,这和之前的假设又矛盾了,很困惑”

  8. 小兴
    小兴 February 9, 2011

    我做了个简单的测试,有两个页面a.php和b.php,并且a.php中require了b.php, 代码如下:
    a.php
    test();
    require ‘b.php’;
    function test()
    {
    echo ‘a.php test’;
    }
    ===========================================
    b.php
    function test()
    {
    //重复定义函数
    }
    程序运行结果是:
    a.php test
    并且抛出一个函数重复定义的致命错误,我的疑问是:
    1. a.php页面在b.php页面抛出了一个致命错误的前提下为什么还能正确输出本页面的test函数结果?
    2. 包含页面和被包含页面之间的编译、执行顺序的原理是怎么样的?
    3. 从运行的结果来看,本页的函数test应该是在执行包含文件操作之前已经被预处理过了,但是我在a.php和b.php页面中都增加了一个函数top,代码如下:
    function top()
    {
    echo ‘top’;
    }
    这个时候a.php页面还是只能运行test函数的结果,不管top函数是在b.php页面中test函数定义之前还是之后,a.php都无法运行top函数,这和之前的假设又矛盾了,很困惑,非常希望能得到你的解惑,谢谢!

  9. 雪候鸟
    雪候鸟 February 8, 2011

    @小兴 include/require都是语句, 是在执行时刻才把被包含的文件纳入进来,编译执行。

  10. 小兴
    小兴 February 8, 2011

    您好,请教一个问题,以前在网上看到过一种说法,说 include其实就是将要被包含的文件全部读入包含文件中,在这个过程中并不解析这些被包含的文件,而是将它们组成了一个大文件后再统一进行解析,我自己测试时发现并不完全是这样,请教您怎样理解include、require和PHP解析流程之间的关系呢,多谢!

  11. heihusdm
    heihusdm September 26, 2010

    学习了,收益匪浅啊

  12. av
    av June 10, 2010

    一直学习中。。…

  13. yufeng
    yufeng May 5, 2010

    学习……这个问题本质上是个寻找文件优先级的问题吧,
    基本上的顺序是:
    1.绝对路径
    2.遍历include_path中的路径
    3.相对于current_script_dir的相对路径
    而且,有两点总结:
    1.只要找到了,就不再向下寻找了。
    2.如果找不到,就一定会向下寻找。即使使用绝对路径,当文件不存在时,PHP依然会按这个次序进入include_path和current_script_dir的相对路径中寻找(当然更找不到了)。

  14. 雪候鸟
    雪候鸟 May 5, 2010

    @N 哦,对, 我设计的例子有问题, 其实是为了说明current_script_dir, 谢谢指正.

  15. dd
    dd May 5, 2010

    主要是在include嵌套的时候,都是以最开始执行的php文件所在的路径为cwd,这样就都理解了。

  16. N
    N May 5, 2010

    >>3. 因为没有了当前路径为include_path, 所以在2.php中包含3.php的时候, 是current_script_dir起了作用, 而current_script_dir是2.php的路径, 所以无论在root还是subdir都将得到”subdir”的输出.
    2.php包含3.php时不应该是path_to_subdir起作用么?

  17. CFC4N
    CFC4N May 5, 2010

    感谢鸟老师,一直学习中。。

  18. tomheng
    tomheng May 4, 2010

    一直在读Laruence老师的博文,很受益。

Comments are closed.