composer require 一行命令就能用别人的库,但 require 'vendor/autoload.php' 背后到底发生了什么?这篇把自动加载的链路拆开,顺便说清楚 PSR-4 的映射规则和几个常见坑。

一、没有自动加载的时候

每个类都要手写 require:

require_once 'lib/Db.php';
require_once 'lib/UserRepository.php';
require_once 'lib/UserService.php';

$service = new UserService(new UserRepository(Db::conn()));

文件一多就是灾难:漏一个就 Class not found,顺序错了还会因为类常量初始化而报错。

二、spl_autoload_register 是核心

PHP 提供了一个机制:当代码里 new 一个不存在的类时,引擎会依次调用注册过的「自动加载函数」。

spl_autoload_register(function (string $class): void {
    $file = __DIR__ . '/lib/' . $class . '.php';
    if (is_file($file)) {
        require_once $file;
    }
});

// 此时才触发自动加载
$service = new UserService();

规则很简单:函数负责把类名映射到文件路径,然后 require 进来。 剩下的事引擎接管。

Composer 做的,就是把这件事做得通用、高效、可配置。

三、vendor/autoload.php 到底加载了什么

打开 vendor/composer/ 目录,会看到几个文件:

vendor/
├── autoload.php
└── composer/
    ├── autoload_real.php
    ├── autoload_psr4.php
    ├── autoload_classmap.php
    ├── autoload_files.php
    ├── autoload_namespaces.php
    └── installed.php

autoload.php 只是入口,真正干活的是 autoload_real.php,它大致做了三件事:

  1. 注册一个 ClassLoader 实例
  2. 把 autoload_psr4.php 里的命名空间映射塞进去
  3. 把 autoload_classmap.php 里的精确映射塞进去,然后 register()

autoload_psr4.php 长这样:

return [
    'Monolog\\'  => [$baseDir . '/monolog/monolog/src/Monolog'],
    'Psr\\Log\\' => [$baseDir . '/psr/log/src'],
    'App\\'      => [$baseDir . '/../src'],
];

autoload_classmap.php 长这样:

return [
    'App\\Kernel' => $baseDir . '/../src/Kernel.php',
    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
];

classmap 是「类名 → 绝对路径」的精确查表,PSR-4 是「命名空间前缀 → 目录」的推导。 查表永远比推导快,这就是为什么 dump-autoload -o 能提速。

四、PSR-4 的映射规则

假设 composer.json 这么配:

{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}

那么:

类名文件路径
App\Kernelsrc/Kernel.php
App\Http\Controller\HomeControllersrc/Http/Controller/HomeController.php
App\Db\PdoFactorysrc/Db/PdoFactory.php

规则是:去掉命名空间前缀 App\,剩下的 \ 换成 /,加上 .php,拼到 src/ 后面。

多目录映射也支持:

{
    "autoload": {
        "psr-4": {
            "App\\": ["src/", "legacy/src/"]
        }
    }
}

Composer 会按数组顺序查找,第一个找到就用。

也可以配多个前缀:

{
    "autoload": {
        "psr-4": {
            "App\\": "src/",
            "App\\Tests\\": "tests/",
            "Acme\\": "lib/acme/"
        }
    }
}

注意前缀最长优先匹配。App\Tests\Foo 会匹配 App\Tests\ 而不是 App\。

五、四种自动加载方式怎么选

{
    "autoload": {
        "psr-4":    { "App\\": "src/" },
        "psr-0":    { "Legacy_": "lib/" },
        "classmap": ["database/seeds", "database/factories"],
        "files":    ["src/helpers.php"]
    }
}
方式适用场景说明
psr-4现代项目,首选命名空间与目录严格对应
psr-0遗留的下划线命名风格已废弃,别在新项目用
classmap目录里文件命名不符合 PSR-4扫描目录生成映射表
files纯函数库(如 helpers.php)每次请求都无条件 require

files 要慎用。它会在每次请求时 require 这些文件,放多了就是纯开销。Laravel 的 helpers.php 就是典型例子,文件很大但每个请求都得加载。

六、三个提速手段

1. 生成优化后的 classmap

composer dump-autoload -o

-o(--optimize)会把所有 PSR-4 能推导出来的类也写进 classmap,运行时直接查表,不再走文件系统推导。

2. 权威 classmap

composer dump-autoload -o --classmap-authoritative

这个参数告诉 Composer:classmap 里没有的类就是不存在,不用再去文件系统找。少了一次 is_file() 检查,是生产环境最快的模式。

代价是:动态生成的类(比如运行时 eval、或者运行时往目录里丢文件)会加载不到。确定项目没这种用法再开。

3. APCu 缓存

composer dump-autoload --apcu

把 classmap 塞进 APCu,多进程共享内存,省掉每次请求读 PHP 文件解析的开销。需要装 APCu 扩展,CLI 下默认不启用(apc.enable_cli=1)。

七、几个真实踩过的坑

坑一:Linux 下大小写敏感

本地 Windows 开发,App\http\Router 能加载 src/Http/Router.php(Windows 文件系统不区分大小写)。部署到 Linux 直接 Class not found。

规范:命名空间和文件名的大小写必须严格一致。 提交前在 CI 里跑一次 Linux 环境的测试能提前发现。

坑二:一个文件多个类

PSR-4 是「一个类一个文件」。如果 src/Utils.php 里塞了 Utils、Str、Arr 三个类,只有 Utils 能被自动加载,另外两个得手动 require。

要么拆文件,要么用 classmap:

{ "autoload": { "classmap": ["src/Utils.php"] } }

坑三:类文件里有副作用

自动加载是「用到才加载」,所以这些副作用什么时候执行是不确定的。类文件里只应该有类定义。

坑四:改了 composer.json 忘了 dump

手动编辑 autoload 配置后,必须执行:

composer dump-autoload

否则 vendor/composer/autoload_psr4.php 还是旧的,新命名空间不生效。这个坑新手最容易踩,因为 composer install 不会重新生成 autoload 映射(除非 composer.lock 变了)。

坑五:生产环境用了 --no-dev 却依赖了 dev 包

composer install --no-dev --optimize-autoloader

--no-dev 不装 require-dev 里的包。如果某个「只在测试用」的类被业务代码意外引用了,本地跑得好好的,上线就炸。所以 require-dev 里的包不要出现在 src/ 中。

八、一张图理清链路

new App\Http\Router()
        │
        ▼
引擎发现类不存在
        │
        ▼
遍历 spl_autoload_register 注册的函数
        │
        ▼
Composer\Autoload\ClassLoader::loadClass()
        │
        ├─ 1. 查 classmap(精确命中,最快)
        │
        ├─ 2. 按 PSR-4 前缀匹配,拼路径,is_file() 检查
        │
        └─ 3. 找到就 includeFile(),找不到就返回
        │
        ▼
类定义进入内存,new 成功

小结

  • spl_autoload_register 是机制,Composer 是它的通用实现
  • PSR-4 靠命名空间前缀推导路径,classmap 是精确查表
  • 生产环境用 composer dump-autoload -o --classmap-authoritative
  • 一个类一个文件、大小写严格一致、类文件不放副作用

理解这三条,Class not found 这类问题基本能自己定位了。