Skip to content

文件路径解析

PHP 的 includerequire 及其 _once 变体在包含文件时,需要根据指定的路径找到目标文件。PHP 支持绝对路径和相对路径,并按照特定顺序搜索文件。理解路径解析规则对于避免"找不到文件"的错误至关重要。

前置知识

基础概念

PHP 在解析包含文件的路径时,遵循以下规则:

  1. 绝对路径:以 /(Unix)或盘符(Windows)开头的路径,直接使用该路径定位文件。
  2. 相对路径:不以 / 开头且不以 ./../ 开头时,PHP 按特定顺序搜索。
  3. __DIR____FILE__ 魔术常量:提供当前文件路径的精确信息。
  4. include_path:当相对路径无法在当前目录找到文件时,搜索 include_path 配置的目录。

路径解析的顺序

对于相对路径,PHP 先在 include_path 中查找(如果 include_path 在 php.ini 中设定),然后按照 include_path 中的顺序逐一搜索。但对于以 ./../ 开头的相对路径,只在当前目录及其子目录中查找。

语法结构

绝对路径

php
<?php
declare(strict_types=1);

// Unix 绝对路径
require_once '/var/www/html/config/database.php';

// Windows 绝对路径
// require_once 'C:\www\project\config\database.php';

// 跨平台写法(使用 DIRECTORY_SEPARATOR)
$basePath = dirname(__DIR__);
$configPath = $basePath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'database.php';
// require_once $configPath;

相对路径

php
<?php
declare(strict_types=1);

// 相对于当前工作目录(不是当前文件所在目录!)
// require_once 'config/database.php';

// 相对于当前文件的目录
require_once __DIR__ . '/config/database.php';

使用 DIRFILE

php
<?php
declare(strict_types=1);

// __FILE__:当前文件的完整路径和文件名
echo "__FILE__ = " . __FILE__ . "\n";

// __DIR__:当前文件所在目录的路径(不含文件名)
echo "__DIR__ = " . __DIR__ . "\n";

// dirname(__FILE__) 等同于 __DIR__(PHP 5.3+)
echo "dirname(__FILE__) = " . dirname(__FILE__) . "\n";

// 上级目录
echo "上级目录 = " . dirname(__DIR__) . "\n";

// 构建项目根目录路径
$rootDir = dirname(__DIR__);
$configPath = $rootDir . '/config/app.php';

echo "配置文件路径:{$configPath}\n";

使用 realpath 规范化路径

php
<?php
declare(strict_types=1);

$filePath = __DIR__ . '/config/database.php';

// realpath() 返回规范化的绝对路径
// 解析符号链接、./ 和 ../
$realPath = realpath($filePath);

if ($realPath !== false) {
    echo "规范化路径:{$realPath}\n";
} else {
    echo "文件不存在:{$filePath}\n";
}

详细说明

文件查找顺序

当使用 include_path 相对路径(不以 ./../ 开头)时,PHP 的查找顺序:

  1. 首先在当前工作目录(不是当前文件目录)查找。
  2. 如果 include_path 中设置了路径,按照 include_path 中的顺序逐一查找。
  3. 如果所有路径都找不到文件,产生 E_WARNING(include)或 E_ERROR(require)。
php
<?php
declare(strict_types=1);

// 查看当前 include_path
echo "include_path:" . get_include_path() . "\n\n";

// 修改 include_path
$pathSeparator = PATH_SEPARATOR; // Unix: :, Windows: ;
$newIncludePath = __DIR__ . '/libs' . $pathSeparator . __DIR__ . '/vendor';
set_include_path(get_include_path() . $pathSeparator . $newIncludePath);

echo "新的 include_path:" . get_include_path() . "\n";

include_path 配置方式

include_path 可以在三个层面配置:

php
<?php
declare(strict_types=1);

// 1. 在 php.ini 中配置
// include_path = ".:/usr/share/php"

// 2. 在 .htaccess 中配置(Apache)
// php_value include_path ".:/usr/share/php"

// 3. 在脚本中动态设置
set_include_path(
    __DIR__ . '/libs' . PATH_SEPARATOR . get_include_path()
);

echo "当前 include_path:\n  " . get_include_path() . "\n";

// 验证:在 include_path 目录中创建文件
$libsDir = __DIR__ . '/libs';
if (!is_dir($libsDir)) {
    mkdir($libsDir, 0755, true);
}

$libFile = $libsDir . '/mylib.php';
file_put_contents($libFile, '<?php echo "从 include_path 加载的库\n";');

// 使用短文件名即可加载(无需完整路径)
include 'mylib.php';

// 清理
unlink($libFile);
rmdir($libsDir);

DIR vs dirname(FILE)

在 PHP 5.3+ 中,__DIR__dirname(__FILE__) 完全等价,但 __DIR__ 更简洁高效。

php
<?php
declare(strict_types=1);

// __DIR__:直接获取当前文件目录
echo "__DIR__: " . __DIR__ . "\n";

// dirname(__FILE__):通过 dirname 函数获取
echo "dirname(__FILE__): " . dirname(__FILE__) . "\n";

// 两者完全等价
// 推荐使用 __DIR__,更简洁

// 获取上级目录
$parentDir = dirname(__DIR__);
echo "上级目录: " . $parentDir . "\n";

相对路径的陷阱

相对路径的不确定性

使用相对路径(如 include 'file.php')时,PHP 基于当前工作目录(可通过 getcwd() 查看)搜索,而不是当前文件所在目录。这意味着从不同目录运行脚本可能导致不同的行为。

php
<?php
declare(strict_types=1);

// 当前工作目录可能不是脚本所在目录
echo "当前工作目录:\n  " . getcwd() . "\n";
echo "当前文件所在目录:\n  " . __DIR__ . "\n";

// 不安全:使用相对路径(基于工作目录)
// require_once 'config/database.php';  // 可能找不到文件

// 安全:使用 __DIR__ 构造绝对路径
require_once __DIR__ . '/config/database.php';

路径解析与 _once 的关系

include_once/require_once 使用文件被解析后的规范化绝对路径来判断文件是否已被包含。这意味着通过不同路径引用同一文件,只要规范化后路径相同,就不会被重复包含。

php
<?php
declare(strict_types=1);

$tempDir = sys_get_temp_dir();
$tempFile = $tempDir . '/path_test.php';
file_put_contents($tempFile, '<?php echo "文件加载\n";');

// 三种方式引用同一文件
// require_once 的 _once 机制使用规范化路径判断
require_once $tempFile;             // 方式 1
require_once $tempDir . '/path_test.php';  // 方式 2
require_once realpath($tempFile);   // 方式 3

echo "所有 require_once 只加载了一次\n";

unlink($tempFile);

实战示例

项目结构中的路径配置

php
<?php
declare(strict_types=1);

// 标准的项目入口文件路径配置
// file: public/index.php

// 定义项目根目录常量
define('APP_ROOT', dirname(__DIR__));

// 核心目录
define('APP_PATH', APP_ROOT . '/app');
define('CONFIG_PATH', APP_ROOT . '/config');
define('PUBLIC_PATH', __DIR__);
define('STORAGE_PATH', APP_ROOT . '/storage');

// 加载 Composer 自动加载
$autoloadPath = APP_ROOT . '/vendor/autoload.php';
if (file_exists($autoloadPath)) {
    require_once $autoloadPath;
}

// 加载配置
$configPath = CONFIG_PATH . '/app.php';
if (file_exists($configPath)) {
    $config = require_once $configPath;
}

echo "项目根目录:" . APP_ROOT . "\n";
echo "应用目录:" . APP_PATH . "\n";
echo "配置目录:" . CONFIG_PATH . "\n";
echo "公共目录:" . PUBLIC_PATH . "\n";

安全的文件加载器

php
<?php
declare(strict_types=1);

class FileLoader
{
    private string $basePath;
    private array $allowedDirs = [];

    public function __construct(string $basePath)
    {
        $this->basePath = realpath($basePath);
    }

    /**
     * 限制只能在指定目录下加载文件
     */
    public function allowDirectory(string $dir): self
    {
        $fullPath = $this->resolvePath($dir);
        if ($fullPath !== false) {
            $this->allowedDirs[] = $fullPath;
        }
        return $this;
    }

    /**
     * 安全地加载文件
     */
    public function load(string $relativePath): mixed
    {
        $fullPath = $this->resolvePath($relativePath);

        if ($fullPath === false) {
            throw new \RuntimeException("文件不存在:{$relativePath}");
        }

        $this->validatePath($fullPath);

        return include $fullPath;
    }

    private function resolvePath(string $path): string|false
    {
        $fullPath = $this->basePath . DIRECTORY_SEPARATOR . ltrim($path, '/\\');
        return realpath($fullPath);
    }

    private function validatePath(string $fullPath): void
    {
        // 检查路径是否在允许的目录内
        if (!empty($this->allowedDirs)) {
            $inAllowed = false;
            foreach ($this->allowedDirs as $dir) {
                if (str_starts_with($fullPath, $dir)) {
                    $inAllowed = true;
                    break;
                }
            }
            if (!$inAllowed) {
                throw new \RuntimeException("不允许加载此目录的文件:{$fullPath}");
            }
        }

        // 防止目录遍历攻击
        if (!str_starts_with($fullPath, $this->basePath)) {
            throw new \RuntimeException("安全限制:不能加载项目外的文件");
        }
    }
}

// 使用示例
$baseDir = sys_get_temp_dir() . '/test_project';
$includesDir = $baseDir . '/includes';

// 创建目录和文件
if (!is_dir($includesDir)) {
    mkdir($includesDir, 0755, true);
}
file_put_contents($includesDir . '/helper.php', '<?php return ["helper" => "loaded"];');

$loader = new FileLoader($baseDir);
$loader->allowDirectory($includesDir);

$result = $loader->load('includes/helper.php');
print_r($result);

// 清理
unlink($includesDir . '/helper.php');
rmdir($includesDir);

动态构建文件路径

php
<?php
declare(strict_types=1);

function resolveIncludePath(string $basePath, string $relativePath): string
{
    // 标准化路径分隔符
    $relativePath = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relativePath);

    // 处理 ./ 前缀
    if (str_starts_with($relativePath, '.' . DIRECTORY_SEPARATOR)) {
        $relativePath = substr($relativePath, 2);
    }

    // 拼接路径
    $fullPath = rtrim($basePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $relativePath;

    // 解析 ../
    $parts = explode(DIRECTORY_SEPARATOR, $fullPath);
    $resolved = [];

    foreach ($parts as $part) {
        if ($part === '..') {
            array_pop($resolved);
        } elseif ($part !== '.' && $part !== '') {
            $resolved[] = $part;
        }
    }

    return implode(DIRECTORY_SEPARATOR, $resolved);
}

// 测试
$base = '/var/www/html';
echo resolveIncludePath($base, 'config/app.php') . "\n";
echo resolveIncludePath($base, './config/app.php') . "\n";
echo resolveIncludePath($base, 'src/../config/app.php') . "\n";

注意事项

  1. 始终使用绝对路径:配合 __DIR__ 构造绝对路径是最安全、最可靠的做法。

  2. 避免使用相对路径:相对路径依赖于当前工作目录,从不同位置运行脚本可能导致不同的结果。

  3. include_path 的现代用途:在现代框架项目中,include_path 的作用已经被 Composer 的自动加载和明确的路径常量取代。

  4. 跨平台兼容性:使用 DIRECTORY_SEPARATOR 常量代替硬编码的 /\

  5. realpath 可返回 false:当文件不存在时,realpath() 返回 false,使用前应检查返回值。

最佳实践

  1. 统一使用 __DIR__ 构造路径:所有 include/require 都使用 __DIR__ 构造绝对路径。

  2. 定义路径常量:在项目入口文件中定义核心目录的路径常量(如 APP_ROOTCONFIG_PATH)。

  3. 使用 Composer 自动加载:类文件加载交给 Composer,减少手动路径管理。

  4. 验证用户提供的路径:当路径包含用户输入时,必须验证路径在允许的范围内。

  5. 使用 realpath() 规范化路径:在比较路径或做安全检查时,先使用 realpath() 规范化。

下一节

PHP 流程控制到此全部结束!回顾一下你学到的所有控制结构:条件语句、循环语句、跳转与终止、文件包含。接下来可以学习 函数 的定义和使用。

参考链接