PHP 基本语法 09|命名空间、引入与自动加载

为什么需要命名空间

没有命名空间时,全世界的 PHP 代码共享一个"全局符号表"。你写一个 Logger 类,用了某个库也定义了 Logger,就冲突了——而且报的是致命错误,且只在两个文件都被加载时才爆。

命名空间给符号加了前缀,把 Logger 变成 App\Logger 和 Vendor\Logger 两回事。

<?php
namespace App\Services;

class Mailer
{
    public function send(): void {}
}

一条硬规则:namespace 必须是文件的第一条语句(declare 可以写在它前面,<?php 不算)。

<?php
declare(strict_types=1);
namespace App\Services;      // ✅ declare 之后、其他任何代码之前

// 下面这行会直接报致命错误
// echo "x";
// namespace App;            // Fatal error: Namespace declaration statement has to be the very first statement

三种引用方式

<?php
namespace App\Http;

// 1) 完全限定名:从全局根开始,最明确
$r = new \App\Repositories\UserRepository();

// 2) 限定名:相对当前命名空间,App\Http\Repositories\...
$r = new Repositories\UserRepository();

// 3) 非限定名:当前命名空间下的 UserRepository
$r = new UserRepository();

// 全局函数/类在命名空间里必须加反斜杠,否则会先找当前命名空间
$now = \time();
$arr = new \ArrayObject([]);

第 3 条是新手最常见的坑:在 namespace App; 里写 new Exception() 会被解析成 App\Exception,找不到就报"Class not found"。要么 use Exception;,要么写 \Exception。

use 导入

<?php
namespace App\Http;

use App\Repositories\UserRepository;
use App\Services\Mailer as MailService;      // 起别名
use function App\Helpers\format_money;        // 导入函数(PHP 5.6+)
use const App\Config\DEBUG;                   // 导入常量

class Controller
{
    public function __construct(
        private UserRepository $repo,        // 直接用短名
        private MailService $mail
    ) {}
}

分组导入(PHP 7.0):

<?php
use App\Repositories\{UserRepository, OrderRepository};
use App\{Services\Mailer, Support\Str};

PSR-4 自动加载:类名和文件路径的约定

现代 PHP 项目不再手写一堆 require。核心是一份 Composer 配置:

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

规则是:命名空间前缀映射到目录,剩余部分按 \ 展开成子目录,类名就是文件名 + .php。

所以:

  • App\Services\Mailer → src/Services/Mailer.php
  • App\Http\Controller\UserController → src/Http/Controller/UserController.php

文件内容:

<?php
namespace App\Services;

class Mailer {}

只要这三处(composer.json 的映射、目录结构、文件里的 namespace)一致,Composer 就能自动找到类。改完 composer.json 记得执行:

composer dump-autoload
# 或生产环境
composer dump-autoload --optimize --classmap-authoritative

--optimize 会生成类映射表,把"按路径找文件"变成"查表",生产环境启动能快不少。

不用 Composer 时的自动加载

<?php
spl_autoload_register(function (string $class): void {
    $prefix = 'App\\';
    if (str_starts_with($class, $prefix)) {
        $path = __DIR__ . '/src/' . str_replace('\\', '/', substr($class, strlen($prefix))) . '.php';
        if (is_file($path)) {
            require $path;
        }
    }
});

这段代码就是 PSR-4 的核心逻辑,理解了它,Composer 的生成文件也就不神秘了。

include / require:四个关键字的差别

找不到文件时
include 警告(Warning),继续执行
require 致命错误(Fatal),停止执行
include_once 同上,但已加载过就跳过
require_once 同上,但已加载过就跳过
<?php
require_once __DIR__ . '/config.php';

引入业务文件一律用 require,不要用 include——找不到配置文件还继续跑,只会引发更难查的错误。

_once 的重复判断是按"解析后的真实路径"来的,所以软链接可能造成判断失效。现代项目用自动加载,_once 的需求基本消失了。

__DIR__ / __FILE__:路径一定用它们

<?php
echo __FILE__;   // 当前文件的绝对路径
echo __DIR__;    // 当前文件所在目录(等价 dirname(__FILE__))
echo __LINE__;   // 当前行号

require __DIR__ . '/../vendor/autoload.php';

不要用相对路径 require './config.php'。相对路径是相对于"当前工作目录",而工作目录取决于你在哪儿执行 php 命令,不是文件的所在位置。这是"本地能跑、线上报错"的经典原因。

__DIR__ 在 5.3 之前不存在,那时候靠 dirname(__FILE__),老代码里能看到这种写法。

函数与常量不被命名空间"包住"的例外

这一点经常让人迷惑:函数和常量在找不到时,会自动回退到全局空间(类不会)。

<?php
namespace App;

echo strlen("abc");      // ✅ 正常,全局函数自动回退
echo MY_CONST;           // ✅ 全局常量也能回退
new Exception();         // ✗ 类不回退,报 Class not found

这个回退机制带来一个性能细节:在热点循环里调用全局函数时,加 \ 前缀会略快,因为省掉了"先找当前命名空间"的查找。

<?php
namespace App;

for ($i = 0; $i < 1000000; $i++) {
    $len = \strlen("x");    // 比 strlen("x") 少一次查找
}

差异很小,但在这类百万级循环里能测出来。别为此改整个项目,热点循环里注意一下就行。

一个完整的骨架

<?php
declare(strict_types=1);

namespace App\Http\Controller;

use App\Repositories\UserRepository;
use App\Support\Response;
use Psr\Http\Message\ServerRequestInterface;

final class UserController
{
    public function __construct(
        private readonly UserRepository $users,
    ) {}

    public function show(ServerRequestInterface $req): Response
    {
        $id = (int)($req->getQueryParams()['id'] ?? 0);
        $user = $this->users->find($id);

        if ($user === null) {
            return Response::notFound("用户不存在");
        }

        return Response::json($user);
    }
}

src/Http/Controller/UserController.php 这个路径,和 App\Http\Controller\UserController 这个类名,是一一对应的。整个项目的结构就是这么长出来的。

坑清单

  1. namespace 前有任何输出(包括空行之后的 echo、BOM 空白)都会报致命错误。
  2. 命名空间里 new Exception() 报 Class not found,要 \Exception 或 use Exception。
  3. 相对路径 require './x.php' 依赖工作目录,线上必炸,用 __DIR__。
  4. 用 include 引入必需的配置文件,出错只是警告,然后代码继续在半残状态下跑。
  5. 改完 composer.json 不 dump-autoload,新类找不到。
  6. 文件名大小写和类名不一致,Linux 上找不到(Windows 上能找到,部署到 Linux 就挂)。
  7. 一个文件写多个命名空间(用 {} 语法)虽然合法,但工具链和可读性都不友好。

顺带说一句第 6 条:这是"本地开发好好的、上线就 500"的头号原因。Windows 的文件系统不区分大小写,Linux 区分。/src/Services/mailer.php 里声明 class Mailer,在 Windows 能找到,在 Linux 就挂。开发环境用 Docker 跑 Linux 容器,能提前发现这类问题。