函数与命名空间
这一课你将能做到
- 写出带类型声明、默认参数、可变参数的现代 PHP 函数,并会匿名函数与箭头函数
- 用 namespace 组织代码,避免全局命名冲突
- 理解 PSR-4 规则,手写 spl_autoload_register 实现自动加载,告别满屏 require
为什么要学
把代码写进函数是复用的第一步,而把函数和类放进命名空间、再用自动加载按需引入,是 PHP 从「脚本」进化到「工程」的分水岭。Laravel、Symfony 这类框架的每一个类都遵循 命名空间 ↔ 目录路径 的 PSR-4 映射——看不懂这个映射,框架源码和 composer.json 就像天书。
人话:函数是「把一段活儿打包」,命名空间是「给代码分小区,防止撞名」,自动加载是「用到哪个文件才去读哪个文件」。三者合起来,就是 PHP 版的 package + import + classloader。
先建立图像
PSR-4 自动加载的核心映射:
composer.json 声明:
"App\\": "src/"
那么:
App\Controllers\UserController
│
▼ 把 \ 换成 / ,加前缀目录
src/Controllers/UserController.php
│
▼ 第一次 new / 调用时自动 require
加载完成,运行
命名空间是逻辑名,目录是物理位置,PSR-4 就是两者之间那张「翻译表」。
跟着做
步骤 1:现代函数写法
新建 fn.php:
<?php
declare(strict_types=1);
// 类型声明 + 默认值 + 可变参数
function discount(float $price, float $rate = 0.9, ...$extra): float {
return round($price * $rate, 2);
}
echo discount(100.0), PHP_EOL; // 90(用默认折扣)
echo discount(100.0, 0.8), PHP_EOL; // 80(覆盖默认值)
echo discount(price: 100.0, rate: 0.85), PHP_EOL; // 85(命名参数,PHP 8+)
// 可变参数收进数组
function sum(int ...$nums): int {
return array_sum($nums);
}
echo sum(1, 2, 3, 4), PHP_EOL; // 10
步骤 2:匿名函数与箭头函数
<?php
declare(strict_types=1);
$users = [
['name' => '张三', 'age' => 28],
['name' => '李四', 'age' => 35],
['name' => '王五', 'age' => 22],
];
// 匿名函数:多行逻辑用 use 捕获外部变量
$minAge = 25;
$adults = array_filter($users, function (array $u) use ($minAge): bool {
return $u['age'] >= $minAge;
});
// 箭头函数:单表达式,自动捕获外部变量(值捕获)
$names = array_map(fn(array $u) => $u['name'], $users);
print_r($names); // 张三、李四、王五
print_r(array_keys($adults)); // 只有张三、李四满足条件
箭头函数 fn() => 是 PHP 7.4+ 语法,等价于自动 use 的单行匿名函数,数组操作场景下是绝对主力。
步骤 3:命名空间解决撞名
建两个文件,故意让类同名:
// src/Alibaba/User.php
namespace Alibaba;
class User {
public function who(): string { return '阿里巴巴的用户'; }
}
// src/Tencent/User.php
namespace Tencent;
class User {
public function who(): string { return '腾讯的用户'; }
}
// main.php
<?php
declare(strict_types=1);
require 'src/Alibaba/User.php';
require 'src/Tencent/User.php';
use Alibaba\User;
use Tencent\User as TUser;
echo (new User())->who(), PHP_EOL; // 阿里巴巴的用户
echo (new TUser())->who(), PHP_EOL; // 腾讯的用户
namespace 声明必须放文件最前;use 是起别名,不等于加载文件;同名类靠命名空间天然隔离。
步骤 4:手写自动加载,告别 require 瀑布
// autoloader.php
<?php
declare(strict_types=1);
spl_autoload_register(function (string $class): void {
// 约定:App\ 开头的类,映射到 src/ 目录
$prefix = 'App\\';
if (str_starts_with($class, $prefix)) {
$relative = substr($class, strlen($prefix));
$file = __DIR__ . '/src/' . str_replace('\\', '/', $relative) . '.php';
if (is_file($file)) {
require $file;
}
}
});
// 假设存在 src/Service/PayService.php,命名空间为 App\Service
// 不再写任何 require,直接 new,类文件自动加载
$pay = new App\Service\PayService();
get_declared_classes() 可验证类是惰性加载的:没用到就不会读文件。Composer 的自动加载就是这套机制的工业级实现。
讲清楚要点
- include/require 本质相同,错误处理不同:找不到文件时
include只给 Warning 继续跑,require抛致命错误终止。生产代码一律require;_once后缀防止重复加载,但性能与心智成本更高,能被自动加载替代就替代。 - 命名空间是逻辑划分,不是目录:
namespace App\Service;本身不产生任何目录或文件行为;只有配合 PSR-4 自动加载,命名空间才和目录一一对应。同一个命名空间可以写在多个文件里。 - PSR-4 只认「前缀 + 子路径」:
"App\\": "src/"表示以App\开头的类,去掉前缀后按\ → /规则去src/找同名.php。类名大小写敏感,文件名必须与类名完全一致。 - 自动加载是惰性的:
spl_autoload_register注册的加载器在类第一次被使用时才触发。所以 PSR-4 项目加载 100 个依赖包也不会拖慢启动——没 new 的类一个文件都不读。
你可能会懵的地方
- use 和 require 是一回事吗?
不是。
use App\Service\User;只是在当前文件给类起短别名,编译期处理,不加载文件;真正加载文件的是 require 或自动加载器。开了自动加载后,use之后直接new即可,无需 require。 - 全局命名空间怎么写?
不写
namespace的文件就在全局命名空间,类名直接可用(如 PHP 内置的\PDO)。在命名空间文件里引用全局类要加反斜杠前缀:\Exception、\DateTimeImmutable。 - PSR-0 和 PSR-4 有什么区别? PSR-0 把命名空间下划线和全部前缀都转成目录(已弃用);PSR-4 只映射去掉前缀后的子路径,目录更浅。现代项目一律 PSR-4。
- 函数也有命名空间吗?
有。
namespace App\Utils; function helper() {...}声明在命名空间内的函数同样隔离。但函数不能被 PSR-4 自动加载(自动加载只针对类/接口/trait/enum),所以工程实践把逻辑都写进类。 - 为什么报错说 class not found,但文件明明在?
三种可能:命名空间声明与目录/前缀映射不一致、类名与文件名大小写不一致(Linux 严格区分)、改了 composer.json 后没跑
composer dump-autoload。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 每个 controller 顶部 10 个 require | 依赖关系手工维护、易漏易重 | PSR-4 + Composer 自动加载 |
use App\User; 后以为文件已加载 | use 不加载任何东西 | 配置自动加载后直接实例化 |
命名空间写成 App/User 或开头带 \ | 语法错误 | 分隔符是 \:App\User |
文件名 userController.php 类名 UserController | 大小写不一致,Linux 上加载失败 | 文件名与类名逐字符一致 |
| 所有代码写在全局命名空间 | 类名冲突、无法被框架加载 | 统一 厂商\项目\模块 命名空间 |
卡点急救
Class "App\xxx" not found:按顺序查三点——composer.json 的autoload.psr-4前缀对不对;命名空间声明与路径是否一致;改完配置是否执行了composer dump-autoload。Cannot redeclare class:同一类被 require 两次。检查是否手动 require 又叠加了自动加载,或误用 include 而非 include_once。- 命名空间内调
date()、PDO报 not found:PHP 会优先在当前命名空间找。全局函数可省略前缀(PHP 有回退机制),全局类必须写全:\PDO、\Throwable。
面试常问
- include 和 require 的区别? 加载行为相同,失败处理不同:include 产生 Warning 后脚本继续执行,require 产生 Fatal Error 直接终止。依赖文件必须用 require;_once 变体防止重复定义,现代项目由 Composer 自动加载统一解决。
- PSR-4 自动加载的映射规则是什么?
composer.json 中
"App\\": "src/"表示:类名以App\开头时,去掉该前缀,把剩余部分的\替换为目录分隔符,在src/下找「完整类名.php」。例如App\Model\Order对应src/Model/Order.php。 - spl_autoload_register 的工作原理? 向 SPL 注册一个或多个自动加载回调,PHP 引擎在遇到未定义类时按注册顺序依次调用,回调负责把类文件 require 进来;类是惰性加载的,未使用的类不产生文件 IO。Composer 的 vendor/autoload.php 本质就是一组预配置的 spl_autoload_register。
自测三问
use语句会触发文件加载吗?它的作用是什么?App\Controller\HomeController在"App\\": "src/"映射下对应哪个文件路径?- 匿名函数和箭头函数在捕获外部变量上的区别是什么?
小结一句话
函数管复用,命名空间管隔离,PSR-4 + spl_autoload_register 管「按需加载」——三者合力把 PHP 脚本变成可维护的工程。
延伸
- PHP 官方手册:命名空间
- 下一课:面向对象——类、接口、trait 与构造器属性提升。