最近把一个旧项目从ThinkPHP 6升级到了8,过程中踩了一些坑,也重新梳理了框架提供的一些“新”能力。其实所谓新,大多是借鉴了主流框架的优秀范式,然后用自己的方式实现。用熟了会发现,TP8在保持轻量和易上手的同时,确实提供了几个能让代码结构变优雅的工具:中间件管请求周期、事件系统解耦业务、服务容器管理依赖。这篇文章就沿着一个具体的API案例,把这些特性串起来讲清楚。
为什么选中这几个点
以前写接口,权限判断、参数校验、日志记录这些逻辑经常散落在控制器基类或者方法开头,时间一长,控制器就臃肿得不行。事件也一样,用户注册后要发邮件、发站内信、记录积分,如果全部写在注册方法里,后续维护简直是灾难。TP8的中间件管道和事件调度机制,恰好是解决这类问题的钥匙。再加上新版对路由、门面、容器的一些优化,整个开发体验比6时代又顺畅了一些。
环境与项目结构
安装依然用composer:
composer create-project topthink/think tp8-api
cd tp8-api
默认的项目目录和TP6差别不大,但一些配置细节有变化。我习惯把API版本分组,所以会在app下建一个api目录,并且多应用模式需要先开启。修改app/provider.php或者直接安装多应用扩展:
composer require topthink/think-multi-app
然后创建app/api目录,里面有自己的controller、middleware、event等。这种隔离方式让单独的API应用不会和其他模块耦合,也方便后续独立部署。
中间件:请求的流水线守护
先来看一个实际需求:对API接口做简单的签名验证。我们定义一个中间件ApiAuth.php,放在app/api/middleware下:
namespace appapimiddleware;
use Closure;
use thinkRequest;
use thinkResponse;
class ApiAuth
{
public function handle(Request $request, Closure $next): Response
{
$appKey = $request->header('X-App-Key');
$timestamp = $request->header('X-Timestamp');
$signature = $request->header('X-Signature');
// 简单校验:签名由 app_secret + timestamp 做sha256
$secret = config('api.app_secret');
if (!$appKey || !$timestamp || !$signature) {
return json(['code' => 401, 'msg' => '缺少认证参数'])->code(401);
}
$expected = hash('sha256', $secret . $timestamp);
if (!hash_equals($expected, $signature)) {
return json(['code' => 403, 'msg' => '签名验证失败'])->code(403);
}
// 时间戳有效期可自行扩展,这里略
return $next($request);
}
}
中间件的写法很直觉:做检查,通过则放行$next($request),否则直接返回响应中断流程。它就像一个请求管道中的阀门。
接着注册到路由中间件,在app/api/middleware.php(没有就新建)中:
return [
appapimiddlewareApiAuth::class,
];
这样一来,该应用下的所有请求都会先经过签名验证。如果想对特定路由组生效,可以只在路由定义时链式调用middleware():
Route::group('/v1', function () {
Route::resource('articles', 'Article');
})->middleware(appapimiddlewareApiAuth::class);
除了认证,日志记录、CORS处理、请求限流等都可以用中间件实现,每个中间件只做一件事,然后组合起来,控制器就会变得非常干净。
事件系统:让业务逻辑各归其位
再来看一个经典场景:用户在某篇文章下发表评论后,要更新文章的评论计数,并且发送通知给作者。如果把这些都写在评论保存方法里,后期加功能就变成“在注释里找修改点”。用事件来解耦就很合适。
首先定义事件类CommentPosted.php,放在app/api/event:
namespace appapievent;
class CommentPosted
{
public function __construct(
public readonly int $articleId,
public readonly int $userId,
public readonly string $content
) {}
}
然后定义两个监听器:一个更新计数,一个发送通知。因为监听器通常跟业务有关,可以放在同一目录或listener子目录。新建UpdateCommentCount.php:
namespace appapievent;
class UpdateCommentCount
{
public function handle(CommentPosted $event): void
{
// 使用模型或Db更新评论数
appapimodelArticle::where('id', $event->articleId)
->inc('comment_count')
->update();
}
}
以及NotifyAuthor.php:
namespace appapievent;
class NotifyAuthor
{
public function handle(CommentPosted $event): void
{
// 查作者,发站内信或邮件,这里仅是示意
$article = appapimodelArticle::find($event->articleId);
if ($article) {
trace("通知作者 {$article->author_id}: 有新评论");
}
}
}
最后,在应用的事件定义文件app/api/event.php中绑定:
return [
'bind' => [],
'listen' => [
appapieventCommentPosted::class => [
appapieventUpdateCommentCount::class,
appapieventNotifyAuthor::class,
],
],
'subscribe' => [],
];
在控制器中触发事件只需一行:
use thinkfacadeEvent;
use appapieventCommentPosted;
// 保存评论后
Event::dispatch(new CommentPosted($articleId, $userId, $content));
这样,评论方法只关心自己的业务,后续增加处理逻辑只需添加新监听器,完全不触及原有代码。而且事件对象是强类型的,不容易出错。
服务容器与依赖注入的实用片段
TP8的容器一直存在,但很多人还是习惯用门面(Facade)或助手函数。这两种方式都没错,不过在复杂业务里,用依赖注入能更容易做单元测试。比如我们有一个ArticleService来处理文章的业务逻辑,可以在控制器中注入:
namespace appapicontroller;
use appapiserviceArticleService;
class Article
{
public function index(ArticleService $service)
{
$list = $service->getList(request()->param('page', 1));
return json($list);
}
}
这个ArticleService可能又依赖模型或其他组件,容器会自动解析其依赖。如果需要绑定接口到实现,在app/service.php中配置:
return [
appapicontractsArticleRepository::class => appapirepositoryArticleDb::class,
];
这样一来,控制器完全不需要知道底层是数据库还是缓存实现,切换起来非常方便。对于中小项目,这不一定是必须的,但了解这种能力有助于在需求膨胀时保持核心代码的整洁。
完整案例:文章管理API
现在我们把上述组件整合成一个可运行的文章管理接口,支持列表、详情、创建、更新、删除,并且使用中间件做认证,使用事件处理后续逻辑。为了简化,这里不引入完整JWT,而是用上面的签名中间件作为示例。
路由定义在route/api.php:
use thinkfacadeRoute;
Route::group('/v1', function () {
Route::get('articles', 'Article/index');
Route::get('articles/:id', 'Article/read');
Route::post('articles', 'Article/save');
Route::put('articles/:id', 'Article/update');
Route::delete('articles/:id', 'Article/delete');
})->middleware(appapimiddlewareApiAuth::class);
控制器app/api/controller/Article.php:
namespace appapicontroller;
use appapiserviceArticleService;
use thinkresponseJson;
class Article
{
protected ArticleService $service;
public function __construct(ArticleService $service)
{
$this->service = $service;
}
public function index(int $page = 1): Json
{
$list = $this->service->getList($page);
return json(['code' => 0, 'data' => $list]);
}
public function read(int $id): Json
{
$article = $this->service->getDetail($id);
if (!$article) {
return json(['code' => 404, 'msg' => '文章不存在'])->code(404);
}
return json(['code' => 0, 'data' => $article]);
}
public function save(): Json
{
$data = request()->post();
$id = $this->service->create($data);
return json(['code' => 0, 'msg' => '创建成功', 'id' => $id]);
}
public function update(int $id): Json
{
$data = request()->put();
$this->service->update($id, $data);
return json(['code' => 0, 'msg' => '更新成功']);
}
public function delete(int $id): Json
{
$this->service->delete($id);
return json(['code' => 0, 'msg' => '删除成功']);
}
}
服务层app/api/service/ArticleService.php:
namespace appapiservice;
use appapimodelArticle as ArticleModel;
use thinkfacadeEvent;
use appapieventArticleCreated;
class ArticleService
{
public function getList(int $page, int $perPage = 15): array
{
return ArticleModel::page($page, $perPage)
->order('id', 'desc')
->select()
->toArray();
}
public function getDetail(int $id): ?array
{
$article = ArticleModel::find($id);
return $article ? $article->toArray() : null;
}
public function create(array $data): int
{
$article = ArticleModel::create($data);
// 触发事件,比如通知管理员或记录日志
Event::dispatch(new ArticleCreated($article->id));
return $article->id;
}
public function update(int $id, array $data): void
{
ArticleModel::update($data, ['id' => $id]);
}
public function delete(int $id): void
{
ArticleModel::destroy($id);
}
}
这里的事件ArticleCreated和对应的监听器你可以自行定义,模式与前面的评论事件一致。模型Article可以基于thinkModel简单建立,此处省略。
这个API骨架虽然简单,但已经包含了认证、服务层隔离、事件驱动等要素。实际开发中,可能还需要加入请求验证(Validate),那也可以做成中间件或者在服务层调用。但核心思想不变:让控制器尽可能薄,业务逻辑交给服务层,横切关注点交给中间件,副作用交给事件。
测试与优化的一些体会
写完接口后,我习惯用Postman做基础功能测试,然后用TP8自带的测试工具写几个Feature测试,尤其是针对中间件和事件。TP8的测试组件基于PHPUnit,可以在tests目录下模拟请求:
php think test
在优化方面,如果API访问量上来,记得把事件监听器中的非关键任务(如发送通知)丢到队列处理,避免阻塞响应。TP8的队列集成也很简单,结合事件使用相得益彰。
另外,中间件的顺序有时会影响结果,比如签名验证最好放在最前面,CORS中间件也要靠前。可以在middleware.php中按顺序排列数组来调整。
结语
ThinkPHP 8不是什么革命性颠覆,但它确实在“让你写更少的代码但保持结构清晰”这件事上做得更好了。中间件、事件、容器这些概念用熟了,项目代码会呈现出一种自然的分层感,而不是把所有东西堆在控制器里。希望这个基于真实场景的串联能帮你更快地把这些特性用到自己的项目里。哪怕只是从一个小事件开始,逐步拆解控制器,代码的可维护性也会有肉眼可见的提升。

