在 PHP 应用里,路由库往往只负责把 HTTP 请求映射到对应的处理函数。Klein 正是这样一个轻量级路由组件,它并不会限制你用控制器、闭包还是函数来响应请求。但很多项目在引入 Klein 之后,仍然把数据库连接、仓储、业务服务全部写在路由闭包里,导致后续想改存储层或写单元测试时非常痛苦。要解决这个问题,关键不是在 Klein 里找内置 DI 容器,而是把独立的依赖注入容器接入到路由分发流程中,让对象创建与业务调用彻底分离。

为什么轻量路由更需要依赖注入
Klein 的典型用法是 $klein->respond('GET', '/users', function ($request, $response) { ... });。这种写法确实短小直接,但它的代价是闭包内部必须自己创建所有依赖。比如你可能会在闭包里写 new PDO(...)、new UserRepository($pdo)、new UserService($repository)。这样一来,路由层就同时承担了配置数据库、组装仓储和执行业务三种职责,任何一个实现变化都要回到路由里改代码。
更重要的是,这种风格几乎无法进行可靠的单元测试。你想测试 UserService 的查询逻辑,就不得不连数据库;你想测试路由是否返回 JSON,就必须真正启动 Web 服务器。依赖注入的核心思想是:类只声明自己需要什么,不负责创建这些依赖。Klein 虽然不提供容器,但我们可以通过 Pimple、League Container 这类轻量容器把对象图管理起来,再让路由闭包仅从容器中取出已经组装好的服务。
从架构角度看,轻量路由比全栈框架更需要显式的 DI 设计。全栈框架往往自带服务容器和自动解析机制,而 Klein 把架构自由度完全交给开发者。如果一开始不把依赖边界划清楚,项目变复杂后再补丁式重构,难度会远大于在初期就引入一个几行代码的容器。
用 Pimple 搭建服务容器并绑定依赖
Pimple 是一个只有单个文件的小型容器,非常适合与 Klein 搭配。安装依赖可以使用 Composer 执行 composer require klein/klein pimple/pimple。之后在引导文件里创建容器,并把每个服务的创建逻辑写成闭包。Pimple 默认会把闭包返回值缓存起来,所以同一个服务在一次请求里只会被实例化一次。
下面是一个完整的基础示例,它把 PDO、用户仓储和用户服务都注册到容器中,然后在 Klein 的路由闭包里解析 userService 并返回 JSON。
<?php
require 'vendor/autoload.php';
use Klein\Klein;
use Pimple\Container;
$container = new Container();
$container['pdo'] = function ($c) {
return new PDO('mysql:host=127.0.0.1;dbname=app', 'root', 'secret');
};
$container['userRepository'] = function ($c) {
return new PdoUserRepository($c['pdo']);
};
$container['userService'] = function ($c) {
return new UserService($c['userRepository']);
};
$klein = new Klein();
$klein->respond('GET', '/users', function ($request, $response) use ($container) {
$users = $container['userService']->getAllUsers();
return $response->json($users);
});
$klein->dispatch();
这段代码已经比在闭包里直接创建对象清晰很多,但仍有一个小问题:路由处理函数仍然需要知道容器的存在,并且要从容器中按字符串键取出服务。对于只有几个路由的项目来说这可以接受,但如果应用继续增长,更推荐把路由处理逻辑迁移到控制器类中,让容器只负责解析控制器。
Pimple 的服务定义闭包接收容器实例作为参数,这种设计可以优雅地实现依赖引用。比如 userService 依赖 userRepository,而 userRepository 又依赖 pdo。当 userService 被取出时,Pimple 会递归创建它依赖的服务。这样我们在业务代码里完全不需要知道 PDO 的配置细节。
在 Klein 路由与控制器中解析依赖
把服务直接注入到闭包虽然可行,但更好的做法是创建控制器类,让控制器在构造函数里声明依赖。Klein 不像一些全栈框架会自动实例化控制器,因此我们需要在路由闭包中从容器解析控制器,或者封装一个更简洁的控制器调度函数。
首先定义一个用户控制器,它只依赖 UserService,不关心服务是怎么创建的:
<?php
class UserController
{
private UserService $userService;
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
public function index($request, $response)
{
$users = $this->userService->getAllUsers();
return $response->json($users);
}
}
然后在容器中注册 userController,并在路由闭包里解析它:
<?php
$container['userController'] = function ($c) {
return new UserController($c['userService']);
};
$klein->respond('GET', '/users', function ($request, $response) use ($container) {
$controller = $container['userController'];
return $controller->index($request, $response);
});
这样做的好处非常明显:路由层只负责绑定 URL 和方法,具体的业务逻辑、数据库访问、结果格式化都放在控制器与服务中。控制器不再继承任何框架基类,也没有静态方法调用,因此可以独立到任何测试环境里验证。对于更大的应用,你还可以写一个通用闭包工厂,根据路由参数动态解析控制器和方法,避免在每一条路由里重复取容器。
当然,使用闭包注入还是控制器注入取决于项目规模。如果只是两三行逻辑的小接口,闭包加容器足够简单;但只要一个接口包含数据校验、事务处理或多步骤调用,就应该把控制器作为依赖注入的边界。这个边界一旦建立,替换存储、模拟外部服务都会非常容易。
通过容器替换实现提升可测试性
引入依赖注入后,最直接的收益是单元测试不再需要真实数据库。以用户控制器测试为例,我们可以在 PHPUnit 中创建 UserService 的 Mock 对象,把预期的用户列表注入到控制器中,然后断言响应 JSON 是否被正确调用。
<?php
use PHPUnit\Framework\TestCase;
class UserControllerTest extends TestCase
{
public function testIndexReturnsJsonUsers()
{
$users = [
['id' => 1, 'name' => 'Alice'],
['id' => 2, 'name' => 'Bob'],
];
$userService = $this->createMock(UserService::class);
$userService->method('getAllUsers')
->willReturn($users);
$controller = new UserController($userService);
$response = $this->createMock(Response::class);
$response->expects($this->once())
->method('json')
->with($users);
$controller->index(null, $response);
}
}
这个测试不连接数据库,也不会发出真实 HTTP 请求。它只验证控制器是否把服务返回的数据交给响应对象。如果 UserService 内部将来改成从缓存、搜索引擎或远程 API 获取数据,这个测试仍然不需要改动。这样就把测试目标从整个应用链路缩小到了单个类的行为。
对于仓储层,也可以利用接口和容器实现替换测试。比如定义 UserRepository 接口,让 PdoUserRepository 实现数据库版本,让 InMemoryUserRepository 实现数组版本。测试服务时注入内存版仓储,生产环境在容器中绑定 PDO 版仓储。因为 Klein 的路由通过容器解析服务,你不需要修改任何路由代码,只需要在测试引导文件中重新绑定条目即可。
要把这套模式固化下来,建议在项目启动文件中统一建立容器,并禁止在路由闭包内使用 new 创建业务对象。对于外部 API 客户端、文件上传、邮件发送这类副作用依赖,也可以定义接口并在服务里通过构造函数注入。这样每一个服务都能独立测试,组合起来时又不会产生隐藏的全局状态。Klein 的灵活性加上依赖注入容器的管理能力,会让小型 PHP 项目在不引入重框架的情况下,依然保持清晰的边界和良好的可测试性。