news 2026/10/10 9:47:12

php文件包含的几种方式总结

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
php文件包含的几种方式总结

前言


PHP 提供四种文件包含写法:include、include_once、require、require_once。它们的差别常被简化成一句「require 比 include 严格」,但这句话没说清严格的到底是什么,也没说清「严格」在失败时具体的表现。真实差别有两条:失败时的错误级别不同,以及失败后脚本还走不走下去。include失败只发一条E_WARNING,脚本继续执行,那个包含表达式的值变成false;require失败产生的是致命错误,脚本当场终止。官方手册把后者的级别描述为E_COMPILE_ERROR。


第二个常见误解是关于_once的:「它比较的是文件名字符串,路径写法不同就挡不住」。这个说法不准确。PHP 内部维护一张「已包含文件」的表,键是解析后的真实路径,也就是经过realpath()归一化的结果。所以./a.php和../dir/a.php指向同一个文件时,include_once确实只包含一次。真正会让_once失效的是完全不同的物理文件(比如同一份代码在多个目录各放了一份),以及用流包装器(stream wrapper)打开的资源——那些没有真实路径可比。


还有一点必须提前点明:文件包含本身是最经典的高危功能之一,一旦被包含的路径来自用户输入,就是本地文件包含漏洞。本文只讲防御侧怎么写,不提供任何利用手法。


一、四种写法的错误级别与返回值


四种写法都是语言结构(language construct),不是函数,所以加括号只是看起来像函数调用,实际不受函数那套规则约束。




写法文件不存在时脚本是否继续是否去重典型用途



include发E_WARNING,表达式值为false继续否可选内容,如模板片段、可选配置

include_once同上继续是可能被重复引入的可选片段

require致命错误,脚本终止终止否必需依赖,如核心类库

require_once同上终止是类、函数定义文件



include有一个容易被忽略的能力:被包含文件里的return语句会给包含表达式提供一个返回值。有return时,include表达式的值就是那个返回值;没有return时值为1。这让它可以直接当配置读取用:


<?php // 适用于 PHP 8.0+
declare(strict_types=1);

// 假设被包含的 config/app.php 里写的是:
// return ['debug' => false, 'timeout' => 30];
//
// 被包含文件里的 return 会成为 include 表达式的值。
// 注意:require 同样支持这个行为,不是 include 独有。
$config = include __DIR__ . '/config/app.php';

// 文件不存在时 include 返回 false,这里必须显式判断
if ($config === false) {
throw new RuntimeException('配置文件缺失');
}

var_dump($config['timeout']); // int(30)

这里有个细节要小心:文件不存在时include返回false,而一个内容为空的成功包含返回1,所以不能用if (!$config)这种宽松判断——如果配置数组恰好是空数组[],它也是假值。


再看失败时的实际表现。下面的输出形态是 PHP 8 下的样子,具体文件名和行号随你的路径而变:


Warning: include(/no/such/file.php): Failed to open stream: No such file or directory
Warning: include(): Failed opening '/no/such/file.php' for inclusion
Fatal error: Uncaught Error: ... (require 才会走到这里)

include的两条警告来自两个不同阶段:第一条是「打开流」失败,第二条是「包含」失败。看到两条警告是正常的,不是重复报错。


二、_once 的去重机制与它的边界


_once变体解决的是一个非常具体的问题:同一个文件被多处以不同路径包含进来,里面的函数或类会被重复声明,触发「Cannot redeclare」的致命错误。


<?php // 适用于 PHP 8.0+
declare(strict_types=1);

// 这两种写法指向同一个物理文件
include_once __DIR__ . '/lib/helper.php';
include_once __DIR__ . '/./lib/../lib/helper.php'; // 第二次不会真的再包含

// 相比之下,普通 include 会真的执行两次
include __DIR__ . '/lib/helper.php';
include __DIR__ . '/./lib/helper.php'; // 若文件里定义了函数,这里就致命错误了

去重的键是解析后的真实路径,所以..、.、多余的斜杠、符号链接都不会让_once失效——符号链接在解析时会被展开成目标路径。


要判断某个文件是否已经被包含过,可以用get_included_files(),它返回本次请求中所有已包含文件的绝对路径数组:


<?php // 适用于 PHP 8.0+
declare(strict_types=1);

require_once __DIR__ . '/lib/helper.php';

$files = get_included_files();
// 数组的第一个元素永远是入口脚本本身
echo '本次请求已包含 ' . count($files) . ' 个文件' . PHP_EOL;

这张表是按请求生命周期维护的,每个请求开始前都会被重置。所以在 PHP-FPM 或 CLI 下,不用担心上一个请求的状态污染本次。


_once也不是没有代价:它每次都要做一次路径解析和查表。这个开销相对于「读取并编译一个 PHP 文件」可以忽略不计,所以不要为了这点开销而改用普通include——用普通include引入定义函数或类的文件,早晚会遇到重复声明的致命错误。


三、路径解析顺序与变量作用域


包含一个相对路径时,PHP 的查找顺序是这样的:



  1. 如果路径以/(Unix)或以盘符、反斜杠(Windows)开头,或者以.、..开头,则完全忽略include_path,直接按当前工作目录解析。

  2. 否则,先在include_path配置的目录列表里逐个查找。

  3. 都没找到,再查调用方脚本自身所在的目录。

  4. 最后查当前工作目录。


第 3 步和第 4 步的存在,是「本地能跑、上线就崩」的经典成因:本地开发时入口脚本和库文件在同一个目录,第 3 步救了场;上线后入口脚本换到了别的位置,同样的相对路径就找不到了。


结论很直接:包含文件永远用__DIR__拼绝对路径。


<?php // 适用于 PHP 8.0+
declare(strict_types=1);

// 可靠:与 cwd 无关,与入口脚本位置无关
require_once __DIR__ . '/lib/Autoloader.php';

// 不可靠:取决于 include_path 和 cwd
// require_once 'lib/Autoloader.php';

include_path的多个目录用PATH_SEPARATOR分隔——Unix 上是冒号,Windows 上是分号。想临时追加目录用set_include_path(),读出来用get_include_path()。默认的include_path里常带一个.,那表示当前工作目录,是一个应当去掉的风险点。


关于变量作用域,规则只有一条:被包含的文件继承包含发生处的变量作用域。在全局作用域包含,文件里就能看到全局变量;在函数里包含,文件里的变量就是那个函数的局部变量,函数返回后就没了。


<?php // 适用于 PHP 8.0+
declare(strict_types=1);

$title = '订单列表';

// 在全局作用域包含,被包含文件里可以直接用 $title
include __DIR__ . '/templates/header.php';

function render(): void
{
$title = '局部标题';

// 在函数内包含,文件里的变量都属于 render() 的局部作用域
include __DIR__ . '/templates/header.php';
}

这个特性被大量模板引擎(包括 PHP 自身当作模板用)依赖。反过来说,被包含的文件里定义的全局变量会「泄漏」到包含处的作用域,这也是它难以调试的原因之一。


四、安全边界:把包含路径关进笼子


文件包含的高危之处在于:一旦被包含的路径可以由用户影响(最常见的是?page=xxx这类写法),攻击者就可能在服务器上包含不该被包含的文件。防御思路有四层,应该叠加使用,而不是只挑一层。


第一层:不要用用户输入当路径。最彻底的方案是白名单映射,把「用户提供的名字」和「真实文件路径」解耦:


<?php // 适用于 PHP 8.0+
declare(strict_types=1);

/**
* 通过白名单把页面标识映射为真实的模板路径。
* 用户永远只能选一个已登记的 key,无法构造出任意路径。
*/
function resolvePage(string $page): string
{
$map = [
'home' => 'home.php',
'about' => 'about.php',
'contact' => 'contact.php',
];

if (!array_key_exists($page, $map)) {
throw new InvalidArgumentException('未知页面');
}

$baseDir = __DIR__ . '/pages';
$realBase = realpath($baseDir);
if ($realBase === false) {
throw new RuntimeException('页面目录不存在');
}

$target = $realBase . DIRECTORY_SEPARATOR . $map[$page];

// 双重保险:即便白名单被改错,也要确认结果没跑出基准目录
$realTarget = realpath($target);
if ($realTarget === false) {
throw new RuntimeException('目标文件不存在');
}

$prefix = $realBase . DIRECTORY_SEPARATOR;
if (!str_starts_with($realTarget, $prefix)) {
throw new RuntimeException('目标路径越界');
}

return $realTarget;
}

$pageName = $_GET['page'] ?? 'home';
// $_GET 的值一定是字符串,但类型不安全;这里显式转成 string 更清楚
include resolvePage((string) $pageName);

注意最后的越界检查必须比到DIRECTORY_SEPARATOR为止。如果只写str_starts_with($realTarget, $realBase),那么当基准目录是/var/www/pages时,/var/www/pages-backup/secret.php也会通过检查——前缀相同但根本不是同一个目录。


第二层:配置层面限制。open_basedir把 PHP 能访问的文件系统范围限制在指定目录内,一旦越界,所有文件函数(包括包含)都会拒绝操作。这是最后一道兜底,但它也会影响file_get_contents()等所有文件操作,配置时要考虑全面。


第三层:关掉远程包含。allow_url_include默认就是Off,永远不要打开。它一旦开启,包含路径就能是 HTTP 或 FTP 地址。配套的allow_url_fopen用于普通文件读取,也应当按需评估,不要为了图方便全局打开。


第四层:入口收敛。让所有包含都发生在少数几个固定位置,用数组或枚举驱动,代码评审时一眼能看出「这个路径是不是可控的」。分散在各处的include $something才是真正的审计噩梦。


关于$_GET的处理还有一点:从 PHP 8.0 起,$_GET、$_POST、$_COOKIE里的值类型仍然一律是字符串或数组(不像某些旧资料说的会自动转成数字),所以「用(int)强转」和「用白名单」这两种做法的差别在于:强转能挡住非数字,但挡不住「数字合法但文件不存在」或者「文件存在但不该被访问」,白名单才是正解。


常见坑点



  1. ❌ 用include引入必须存在的核心类库。


✅ 文件缺失时include只发警告就继续往下跑,后续会以「类未定义」的形式在更远的地方崩掉,排查成本高。必须存在的依赖一律用require。



  1. ❌ 用if (!include 'config.php')判断配置是否加载成功。


✅ 文件成功包含但没有return时,表达式的值是1;用了return且返回空数组[]时也是假值。应当判断=== false,或者去读return的返回值类型。



  1. ❌ 反复用include引入定义函数或类的文件。


✅ 第二次会触发「Cannot redeclare」致命错误。定义类、函数的文件必须用require_once或include_once。



  1. ❌ 用相对路径包含,例如include 'lib/Utils.php'。


✅ 相对路径的基准可能是include_path、调用脚本目录或当前工作目录,取决于路径是否以.开头,行为随部署环境变化。一律用__DIR__拼绝对路径。



  1. ❌ 把$_GET['page']直接拼进include的路径里。


✅ 这是典型的本地文件包含入口。用白名单映射把标识和真实路径解耦,再加realpath()与基准目录前缀比对做兜底。



  1. ❌ 越界检查写成str_starts_with($realPath, $baseDir)。


✅ 前缀比对必须带上目录分隔符,否则/var/www/pages-backup会被误判为在/var/www/pages之内。应写成str_starts_with($realPath, $baseDir . DIRECTORY_SEPARATOR)。



  1. ❌ 为了「支持远程模板」把allow_url_include打开。


✅ 该选项默认Off,开启后包含路径可以是远程地址,风险极高。需要远程资源就先用 HTTP 客户端拉取到本地临时文件,再按本地文件的规则校验后包含。



  1. ❌ 在循环里include同一个渲染片段,并在片段里定义函数。


✅ 循环会让文件被重复执行。要么用include_once,要么把函数定义移出片段,让片段只做输出。


总结




关注点结论



失败时的错误级别include为E_WARNING且继续执行;require为致命错误并终止

失败时的返回值include表达式为false;用=== false判断最稳

成功时的返回值有return则取该值,否则为1

_once去重依据解析后的真实路径,..、多余斜杠、符号链接都不影响

相对路径查找顺序include_path→ 调用脚本目录 → 当前工作目录

变量作用域继承包含发生处的作用域,函数内包含即为局部变量

推荐写法一律require_once __DIR__ . '/x.php'

安全底线白名单映射 +realpath()前缀比对 +open_basedir+allow_url_include=Off



四种包含写法的选择其实没有争议:定义类与函数的文件用require_once,可选的模板片段用include,路径一律用__DIR__拼绝对路径。真正需要花心思的不是选哪个关键字,而是回答「这个路径会不会被用户影响」——只要答案是「会」,白名单就是唯一可靠的答案。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 9:45:41

OpenClaw操控浏览器全解析:原理、落地与避坑指南

当看到"OpenClaw能操控浏览器"这个话题的时候&#xff0c;我第一反应是&#xff1a;智能体圈子里一直在喊的"AI替你干活"&#xff0c;这回终于不再是演示Demo了。OpenClaw作为一个开源的个人AI代理框架&#xff0c;最大的特点就是它不满足于在对话框里给你…

作者头像 李华
网站建设 2026/10/10 9:44:36

Git 2.53 diff加速:Rust重写如何让大仓库性能飞跃

按下回车之后&#xff0c;终端光标跳回下一行&#xff0c;开始慢慢吐出改动列表。那段停顿大概持续了两三秒&#xff0c;在一个积累了多年的老仓库里&#xff0c;git diff 的每一次停顿都会被放大——特别是我在做代码评审的最后一轮&#xff0c;只是想确认这次跨模块改动到底碰…

作者头像 李华
网站建设 2026/10/10 9:44:10

生成式AI赋能测试结果分析:从日志洪流到精准归因的智能实践

1. 痛点拆解&#xff1a;测试结果分析到底难在哪里先说说我为什么对这个话题这么上心。干了这么多年测试&#xff0c;从手工测试到自动化测试&#xff0c;再到现在的测试开发&#xff0c;我越来越觉得一个尴尬的事实&#xff1a;自动化测试把"执行"这个环节解放了&am…

作者头像 李华
网站建设 2026/10/10 9:44:07

人机协同写作素养:AI写作的能力边界与四步工作流

你是不是也遇到过这样的场景&#xff1a;打开AI写作工具&#xff0c;输入一句“帮我写一篇行业分析报告”&#xff0c;几秒钟后屏幕里蹦出一篇结构工整、措辞通顺的长文&#xff0c;可读完之后总觉得哪里不对劲——观点似曾相识、案例浮在表面、细节经不起推敲&#xff0c;你甚…

作者头像 李华
网站建设 2026/10/10 9:42:37

Git急救指南:用reflog和fsck找回误删的提交与文件

1. 看懂 Git 的“后悔药”原理先说结论&#xff1a;Git 之所以能急救&#xff0c;是因为它根本不是你以为的那种“版本管理工具”&#xff0c;而是一个内容寻址的对象数据库。分支名、HEAD、标签这些你天天打交道的概念&#xff0c;本质上只是一串指向对象库的指针。你每一次co…

作者头像 李华
网站建设 2026/10/10 9:42:11

HslCommunication v11.3.2:.NET 4.5 工业通信库的轻量部署与 PLC 协议实战

简介&#xff1a;HslCommunication 是一款面向工业自动化与上位机开发者的高性能 .NET 通信类库&#xff0c;适用于 C# 开发者快速实现与 PLC、Modbus 设备、OPC UA 服务器等工业设备的数据交互。本资源提供官方 v11.3.2 稳定版&#xff08;基于 .NET Framework 4.5&#xff09…

作者头像 李华