Skip to content

PSR-4 自动加载

PSR-4(PHP Standard Recommendation 4)是 PHP-FIG(PHP Framework Interop Group)制定的自动加载标准,定义了从命名空间到文件目录的映射规则。通过 PSR-4,你只需 require 一个 vendor/autoload.php 文件,就可以使用项目中任何类,无需手动编写大量 requireinclude 语句。本节将全面介绍 PSR-4 规范、Composer 中的 autoload 配置,以及在实际项目中的应用。

前置知识

在阅读本节之前,你需要了解:

什么是 PSR-4

PSR-4 的全称是 "Autoloading Standard",它定义了一套从 命名空间 映射到 文件系统路径 的规则。核心思想是:

命名空间中的每一个 \ 分隔符对应文件系统中的一个目录分隔符 /,最终的类名(不包含命名空间前缀)对应文件名。

PSR-4 要求:

  1. 一个完全限定的类名(FQCN)具有如下形式:\<NamespacePrefix>(\<SubNamespaceNames>)*\<ClassName>
  2. 命名空间前缀对应一个或多个"基础目录"
  3. 命名空间前缀之后的连续子命名空间对应基础目录中的子目录
  4. 最终的类名对应一个以 .php 为后缀的文件

映射规则图解

命名空间: App\Http\Controllers\UserController
对应路径:  app/Http/Controllers/UserController.php

规则分解:
App          → 基础目录前缀(对应 src/ 或 app/)
\Http        → Http 子目录
\Controllers → Controllers 子目录
\UserController → UserController.php

命名空间与文件路径的映射

php
<?php
declare(strict_types=1);

// 文件:app/Http/Controllers/UserController.php
namespace App\Http\Controllers;

class UserController
{
    public function index(): void
    {
        echo "User Controller";
    }
}

// 使用时:
// require_once 'vendor/autoload.php';
// $controller = new \App\Http\Controllers\UserController();
// $controller->index();

PSR-4 vs PSR-0

PSR-4 是 PSR-0 的后继标准,两者主要区别:

  • PSR-0 允许在命名空间中使用下划线 _(已废弃),PSR-4 不允许
  • PSR-0 要求类名中包含下划线 _ 映射到目录分隔符,PSR-4 取消了这个规则
  • PSR-4 的映射规则更简洁,完全依赖命名空间到目录的对应
  • PSR-0 已于 2014 年被废弃,现在应统一使用 PSR-4

Composer 中的 autoload 配置

psr-4 配置

composer.json 中通过 autoload.psr-4 配置命名空间到目录的映射:

json
{
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Domain\\": "src/Domain/",
            "Infrastructure\\": "src/Infrastructure/"
        }
    }
}

配置格式:"命名空间前缀\\": "目录路径/"

命名空间中的反斜杠

在 JSON 中,反斜杠 \ 需要转义为 \\。所以 App\ 在 JSON 中写为 "App\\"

目录结构示例

my-project/
├── composer.json
├── vendor/
│   └── autoload.php          ← Composer 生成的自动加载入口
├── app/                       ← App\ 命名空间根目录
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── UserController.php    ← App\Http\Controllers\UserController
│   │   │   └── OrderController.php    ← App\Http\Controllers\OrderController
│   │   ├── Middleware/
│   │   │   └── AuthMiddleware.php     ← App\Http\Middleware\AuthMiddleware
│   │   └── Requests/
│   │       └── StoreUserRequest.php   ← App\Http\Requests\StoreUserRequest
│   ├── Models/
│   │   ├── User.php                    ← App\Models\User
│   │   └── Order.php                   ← App\Models\Order
│   ├── Services/
│   │   ├── PaymentService.php          ← App\Services\PaymentService
│   │   └── EmailService.php            ← App\Services\EmailService
│   └── Providers/
│       └── AppServiceProvider.php     ← App\Providers\AppServiceProvider
└── src/
    ├── Domain/
    │   ├── User/
    │   │   ├── UserId.php              ← Domain\User\UserId
    │   │   └── UserRepository.php       ← Domain\User\UserRepository
    │   └── Order/
    │       ├── OrderId.php             ← Domain\Order\OrderId
    │       └── OrderRepository.php      ← Domain\Order\OrderRepository
    └── Infrastructure/
        ├── Persistence/
        │   └── MySqlUserRepository.php ← Infrastructure\Persistence\MySqlUserRepository
        └── Http/
            └── GuzzleHttpClient.php    ← Infrastructure\Http\GuzzleHttpClient

类文件编写规范

php
<?php
declare(strict_types=1);

// 文件路径: app/Http/Controllers/UserController.php
// 命名空间: App\Http\Controllers

namespace App\Http\Controllers;

use App\Models\User;
use App\Services\UserService;

class UserController
{
    public function __construct(
        private readonly UserService $userService,
    ) {}

    public function index(): array
    {
        return $this->userService->getAllUsers();
    }

    public function show(int $id): ?User
    {
        return $this->userService->findUser($id);
    }

    public function store(array $data): User
    {
        return $this->userService->createUser($data);
    }

    public function update(int $id, array $data): ?User
    {
        return $this->userService->updateUser($id, $data);
    }

    public function destroy(int $id): bool
    {
        return $this->userService->deleteUser($id);
    }
}
php
<?php
declare(strict_types=1);

// 文件路径: app/Models/User.php
// 命名空间: App\Models

namespace App\Models;

class User
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly string $email,
        public readonly \DateTimeImmutable $createdAt,
    ) {}

    public function toArray(): array
    {
        return [
            'id'         => $this->id,
            'name'       => $this->name,
            'email'      => $this->email,
            'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
        ];
    }
}
php
<?php
declare(strict_types=1);

// 文件路径: app/Services/UserService.php
// 命名空间: App\Services

namespace App\Services;

use App\Models\User;
use Domain\User\UserRepository;
use Domain\User\UserId;

class UserService
{
    public function __construct(
        private readonly UserRepository $userRepository,
    ) {}

    public function getAllUsers(): array
    {
        // 业务逻辑
        return [];
    }

    public function findUser(int $id): ?User
    {
        $userId = new UserId($id);
        return $this->userRepository->findById($userId);
    }

    public function createUser(array $data): User
    {
        // 创建用户逻辑
        // ...
        return new User(1, $data['name'], $data['email'], new \DateTimeImmutable());
    }

    public function updateUser(int $id, array $data): ?User
    {
        // 更新用户逻辑
        return $this->findUser($id);
    }

    public function deleteUser(int $id): bool
    {
        $userId = new UserId($id);
        $this->userRepository->delete($userId);
        return true;
    }
}

classmap 自动加载

除了 PSR-4,Composer 还支持 classmap 方式加载类。它会扫描指定目录,建立类名到文件路径的映射。

json
{
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        },
        "classmap": [
            "src/legacy/",
            "src/functions.php"
        ]
    }
}

classmap 适用于:

  • 不符合 PSR-4 规范的旧代码
  • 函数库文件(如包含大量全局函数的文件)
  • 不使用命名空间的遗留代码

classmap 的缺点

  • classmap 需要在每次修改文件后重新生成(运行 composer dump-autoload
  • 不支持新增文件的热加载
  • 映射关系不透明,不如 PSR-4 直观
  • 仅在没有其他选择时使用

files 手动加载

files 配置允许你指定在每次请求时都自动加载的文件:

json
{
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        },
        "files": [
            "app/helpers.php",
            "app/constants.php"
        ]
    }
}

示例文件 app/helpers.php

php
<?php
declare(strict_types=1);

// 全局辅助函数

if (!function_exists('array_get')) {
    /**
     * 使用点号语法从数组中获取值
     */
    function array_get(array $array, string $key, mixed $default = null): mixed
    {
        $keys = explode('.', $key);
        $value = $array;

        foreach ($keys as $segment) {
            if (!is_array($value) || !array_key_exists($segment, $value)) {
                return $default;
            }
            $value = $value[$segment];
        }

        return $value;
    }
}

if (!function_exists('str_slug')) {
    /**
     * 生成 URL 友好的 slug
     */
    function str_slug(string $title, string $separator = '-'): string
    {
        $title = preg_replace('/[^\p{L}\p{N}\s]/u', '', $title);
        $title = preg_replace('/[\s-]+/', $separator, $title);
        return strtolower(trim($title, $separator));
    }
}

if (!function_exists('response_json')) {
    /**
     * 返回 JSON 响应
     */
    function response_json(array $data, int $statusCode = 200): void
    {
        http_response_code($statusCode);
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
        exit;
    }
}

files 的使用场景

files 应该谨慎使用,因为它会在每次请求时加载指定的文件(而非按需加载)。推荐的使用场景:

  • 全局辅助函数库
  • 定义常量
  • 启动引导文件

不应滥用 files 来加载大量类文件,那应该使用 PSR-4 的按需加载机制。

autoload 与 autoload-dev 的区别

json
{
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Domain\\": "src/Domain/"
        },
        "files": [
            "app/helpers.php"
        ]
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/",
            "Fixtures\\": "tests/Fixtures/"
        }
    }
}
属性autoloadautoload-dev
用途生产环境和开发环境仅开发环境
何时加载始终加载仅在安装了开发依赖时加载
典型内容应用代码、库代码、辅助函数测试类、测试夹具、Mock
生产部署包含使用 --no-dev 排除
bash
# 开发环境安装所有依赖
composer install

# 生产环境安装(排除开发依赖,autoload-dev 中的内容不生成自动加载)
composer install --no-dev --optimize-autoloader

dump-autoload 命令

当修改了 composer.json 中的 autoload 配置,或新增/删除了类文件后,需要重新生成自动加载文件:

bash
# 重新生成自动加载文件
composer dump-autoload

# 等价于
composer dump-autoload

# 优化自动加载(生成 classmap)
# 将命名空间到文件的映射缓存为静态 classmap
# 生产环境推荐使用
composer dump-autoload -o

# 或
composer dump-autoload --optimize

# 不优化(开发环境推荐)
# 每次请求实时查找文件,方便开发调试
composer dump-autoload --no-dev

优化模式的工作原理

使用 -o(优化模式)时,Composer 会扫描所有 PSR-4 配置的目录,为每个类生成一个精确的 classmap 缓存。这意味着:

  • 类加载更快(无需每次实时查找目录)
  • 新增的类需要重新运行 dump-autoload 才能被发现
  • 文件大小会增加

开发环境建议不使用 -o,以便新增的类能被立即发现。

实战示例:完整的项目目录结构

下面是一个遵循 PSR-4 规范的中型 PHP 项目结构:

my-project/
├── composer.json
├── composer.lock
├── .env.example
├── phpunit.xml
├── public/
│   └── index.php              ← Web 入口
├── app/                       ← "App\" 命名空间
│   ├── bootstrap.php          ← 应用引导文件
│   ├── helpers.php            ← 全局辅助函数
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── HomeController.php
│   │   │   ├── UserController.php
│   │   │   └── ApiController.php
│   │   ├── Middleware/
│   │   │   ├── AuthMiddleware.php
│   │   │   └── CorsMiddleware.php
│   │   └── Requests/
│   │       └── UserFormRequest.php
│   ├── Models/
│   │   ├── User.php
│   │   └── Post.php
│   ├── Services/
│   │   ├── UserService.php
│   │   └── PaymentService.php
│   ├── Repositories/
│   │   ├── UserRepository.php
│   │   └── PostRepository.php
│   └── Exceptions/
│       ├── NotFoundException.php
│       └── ValidationException.php
├── src/                       ← 额外的命名空间
│   ├── Domain/
│   │   ├── Events/
│   │   │   └── UserRegistered.php
│   │   └── ValueObjects/
│   │       ├── Email.php
│   │       └── Money.php
│   └── Support/
│       ├── Collection.php
│       └── Str.php
├── tests/                     ← "Tests\" 命名空间(autoload-dev)
│   ├── Unit/
│   │   ├── Http/
│   │   │   └── Controllers/
│   │   │       └── UserControllerTest.php
│   │   ├── Models/
│   │   │   └── UserTest.php
│   │   └── Services/
│   │       └── UserServiceTest.php
│   ├── Feature/
│   │   └── UserRegistrationTest.php
│   └── Fixtures/
│       └── UserFixture.php
├── config/
│   ├── app.php
│   └── database.php
└── storage/
    ├── logs/
    └── cache/

对应的 composer.json

json
{
    "name": "myorg/my-project",
    "description": "A well-structured PHP application",
    "type": "project",
    "require": {
        "php": "^8.1",
        "ext-pdo": "*",
        "ext-json": "*",
        "ext-mbstring": "*",
        "vlucas/phpdotenv": "^5.5",
        "guzzlehttp/guzzle": "^7.5"
    },
    "require-dev": {
        "phpunit/phpunit": "^10.0",
        "phpstan/phpstan": "^1.10"
    },
    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Domain\\": "src/Domain/",
            "Support\\": "src/Support/"
        },
        "files": [
            "app/helpers.php",
            "app/bootstrap.php"
        ]
    },
    "autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/",
            "Tests\\Fixtures\\": "tests/Fixtures/"
        }
    },
    "scripts": {
        "test": "phpunit",
        "phpstan": "phpstan analyse app src --level=8",
        "check": ["@phpstan", "@test"]
    },
    "config": {
        "optimize-autoloader": true,
        "sort-packages": true
    },
    "minimum-stability": "stable",
    "prefer-stable": true
}

入口文件 public/index.php

php
<?php
declare(strict_types=1);

// 加载 Composer 自动加载
require __DIR__ . '/../vendor/autoload.php';

// 加载环境变量
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->safeLoad();

// 引导应用
$app = require __DIR__ . '/../app/bootstrap.php';

// 路由分发
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];

// 简易路由匹配
$routes = require __DIR__ . '/../config/routes.php';

$matched = false;
foreach ($routes as [$routeMethod, $routePath, $handler]) {
    if ($method !== $routeMethod) {
        continue;
    }

    $pattern = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $routePath);
    $regex = '~^' . $pattern . '$~';

    if (preg_match($regex, $uri, $matches)) {
        $matched = true;
        [$controllerClass, $action] = $handler;
        $controller = new $controllerClass();
        echo $controller->$action(...array_slice(array_values($matches), 1));
        break;
    }
}

if (!$matched) {
    http_response_code(404);
    echo json_encode(['error' => 'Not Found']);
}

注意事项

1. 文件名必须与类名一致

php
<?php
// 文件名: User.php
namespace App\Models;
class User {}    // 正确 ✓

// 文件名: user.php (小写)
// 在 Linux/macOS 下可以工作(文件系统不区分大小写取决于文件系统)
// 在严格区分大小写的文件系统上可能失败
// 建议:始终保持文件名与类名大小写一致

2. 一个文件一个类

PSR-4 规范要求每个文件只包含一个类,虽然技术上可以在一个文件中定义多个类,但不推荐:

php
<?php
// 不推荐:一个文件中定义多个类
namespace App\Models;

class User {}
class Admin extends User {}
class Guest extends User {}

// 推荐:每个类单独一个文件
// User.php → class User
// Admin.php → class Admin
// Guest.php → class Guest

3. 命名空间前缀末尾的反斜杠

json
{
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        }
    }
}

命名空间前缀末尾必须有 \\(JSON 中转义为 \\\\),它表示这是一个命名空间前缀,而不是完整的命名空间。没有这个反斜杠,PSR-4 会把整个字符串当作前缀匹配。

4. Composer 生成的自动加载文件

vendor/
├── autoload.php              ← 你需要 require 的唯一文件
├── composer/
│   ├── autoload_classmap.php    ← classmap 映射(-o 后生成)
│   ├── autoload_namespaces.php  ← PSR-0 映射(已废弃)
│   ├── autoload_psr4.php       ← PSR-4 映射
│   ├── autoload_static.php     ← 静态 classmap
│   ├── autoload_files.php      ← files 配置中的文件列表
│   ├── ClassLoader.php         ← Composer 的自动加载器类
│   └── installed.json          ← 已安装包的信息
└── ...

最佳实践

1. 保持命名空间与目录结构一致

php
<?php
// 命名空间和目录完全对应
// App\Http\Controllers\UserController → app/Http/Controllers/UserController.php
// App\Services\PaymentService → app/Services/PaymentService.php
// Domain\Events\UserRegistered → src/Domain/Events/UserRegistered.php

// 避免出现不一致:
// 命名空间是 App\Services 但文件放在 app/Service(缺少 s)
// 这种不一致会导致 PSR-4 自动加载失败

2. 使用有意义的命名空间前缀

json
{
    "autoload": {
        "psr-4": {
            "MyCompany\\MyProject\\": "src/",
            "MyCompany\\Shared\\": "packages/shared/src/"
        }
    }
}

3. 使用 Composer 插件检查命名空间

bash
# 安装 PHP-CS-Fixer 或 PHP_CodeSniffer 来检查 PSR-4 合规性

# PHP-CS-Fixer
composer require --dev friendsofphp/php-cs-fixer
vendor/bin/php-cs-fixer fix --rules=psr4

# PHP_CodeSniffer + PSR-12 sniff
composer require --dev squizlabs/php_codesniffer
vendor/bin/phpcs --standard=PSR12 src/

4. 开发环境关闭优化,生产环境开启

bash
# 开发环境(便于新增类文件后无需 dump-autoload)
composer dump-autoload

# 生产环境(类加载更快)
composer dump-autoload -o

下一节

你已经掌握了 PSR-4 自动加载的完整规范和实战配置。至此,PHP 入门的"运行时与 Composer"部分已经完成。接下来建议你继续学习 PHP 语言基础部分。

参考链接