在 ThinkPHP 8 的众多新特性中,注解路由与自动依赖注入无疑是提升开发体验的两大利器。注解路由让我们告别繁琐的路由配置文件,直接在控制器方法上通过注释声明路由规则;而增强的依赖注入容器让类的创建和依赖管理变得前所未有的简单。本文将通过构建一个文章管理 RESTful API的完整案例,手把手带你掌握这两项特性,并结合模型事件、服务抽象等最佳实践,打造出清晰、可测试的现代 PHP 应用。
为什么需要注解路由与依赖注入?
传统的 ThinkPHP 路由需要在route/app.php中逐条定义,当控制器和接口数量增长时,这个文件会变得臃肿且难以维护。而注解路由将路由规则直接写在控制器的注释中,实现了“路由随代码走”的效果,不仅减少了文件跳转,也让接口定义更加内聚。
依赖注入方面,ThinkPHP 8 的容器能够根据类型声明自动解析并注入依赖。你不再需要在构造函数中手动实例化服务类,只需在参数中声明类型,框架会自动完成依赖的加载和注入。这使得单元测试变得极其简单——你可以轻松替换任一依赖为模拟对象。
接下来,我们先分别了解这两项技术的基础,然后将它们融入完整的 API 开发实战中。
注解路由基础:从配置文件到注释
要在 ThinkPHP 8 中使用注解路由,首先确保安装了注解扩展包(通常项目已内置)。接下来,你可以在控制器方法上使用#[Route]属性(PHP 8 原生注解)或 PHPDoc 风格的@route注释。本文采用 PHP 8 属性方式,因为它更现代且支持类型提示。
一个简单的注解路由示例:
// app/controller/Article.php
namespace appcontroller;
use thinkannotationRoute;
class Article
{
#[Route('GET', '/api/articles')]
public function index()
{
return json(['data' => []]);
}
#[Route('GET', '/api/articles/<id>')]
public function read(int $id)
{
return json(['data' => ['id' => $id]]);
}
#[Route('POST', '/api/articles')]
public function save()
{
return json(['msg' => '创建成功']);
}
#[Route('PUT', '/api/articles/<id>')]
public function update(int $id)
{
return json(['msg' => "更新文章 {$id}"]);
}
#[Route('DELETE', '/api/articles/<id>')]
public function delete(int $id)
{
return json(['msg' => "删除文章 {$id}"]);
}
}
注意,使用#[Route]属性需要引入use thinkannotationRoute;,并且控制器所在目录需要有annotation路由加载配置。在route/app.php中添加一行即可开启:
// route/app.php
use thinkfacadeRoute;
Route::group('api', function () {
// 加载注解路由
})->allowCrossDomain();
更推荐的方式是在config/route.php中设置'annotation' => true,这样框架会自动扫描控制器目录中的路由注解,无需额外配置。
依赖注入容器与类型声明自动解析
ThinkPHP 8 的容器是一个强大的依赖管理工具。当你在控制器方法中声明一个参数类型,而这个类型不是请求参数时,容器会自动尝试从容器中解析该类的实例。最常见的用法是在构造函数中注入服务类,然后在整个控制器中使用。
定义一篇文章服务接口及其实现:
// app/service/ArticleService.php (接口)
namespace appservice;
interface ArticleService
{
public function getList(int $page, int $limit): array;
public function getById(int $id): ?array;
public function create(array $data): int;
public function updateById(int $id, array $data): bool;
public function deleteById(int $id): bool;
}
// app/service/ArticleServiceImpl.php (实现类)
namespace appservice;
use appmodelArticle;
class ArticleServiceImpl implements ArticleService
{
public function getList(int $page, int $limit): array
{
return Article::page($page, $limit)->select()->toArray();
}
public function getById(int $id): ?array
{
$article = Article::find($id);
return $article ? $article->toArray() : null;
}
public function create(array $data): int
{
$article = Article::create($data);
return $article->id;
}
public function updateById(int $id, array $data): bool
{
return Article::update($data, ['id' => $id]) !== false;
}
public function deleteById(int $id): bool
{
return Article::destroy($id) > 0;
}
}
通常我们需要在服务提供者中绑定接口与实现:
// app/provider.php (或在Service中)
use thinkService;
use appserviceArticleService;
use appserviceArticleServiceImpl;
class AppService extends Service
{
public function register()
{
$this->app->bind(ArticleService::class, ArticleServiceImpl::class);
}
}
然后在控制器中,只需通过构造函数注入接口即可:
// app/controller/Article.php
namespace appcontroller;
use appserviceArticleService;
class Article
{
protected ArticleService $articleService;
public function __construct(ArticleService $articleService)
{
$this->articleService = $articleService;
}
// ... 接下来的方法直接使用 $this->articleService
}
这种设计让控制器与具体实现解耦,你可以轻易替换实现类(例如用缓存实现),而无需修改控制器代码。
实战案例:文章管理 RESTful API
现在我们将注解路由和依赖注入结合起来,构建一个完整的文章增删改查接口,并加入模型事件自动处理 Slug 生成和缓存清理。
项目结构与数据表设计
数据库表article结构:
CREATE TABLE `article` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`title` varchar(200) NOT NULL,
`content` text NOT NULL,
`slug` varchar(200) NOT NULL,
`status` tinyint DEFAULT 1 COMMENT '1:草稿 2:已发布',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `slug` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
模型定义与模型事件
创建模型app/model/Article.php,并利用模型事件在创建/更新时自动生成 URL 友好的 Slug:
// app/model/Article.php
namespace appmodel;
use thinkModel;
class Article extends Model
{
protected $autoWriteTimestamp = true;
protected $createTime = 'created_at';
protected $updateTime = 'updated_at';
// 模型事件:新增前
public static function onBeforeInsert($article)
{
if (empty($article->slug)) {
$article->slug = self::generateSlug($article->title);
}
// 确保唯一性
$article->slug = self::makeUniqueSlug($article->slug, $article->id ?? 0);
}
public static function onBeforeUpdate($article)
{
if ($article->isDirty('title')) {
$article->slug = self::makeUniqueSlug(
self::generateSlug($article->title),
$article->id
);
}
}
private static function generateSlug(string $title): string
{
// 简单转拼音或直接使用英文slug转换,此处用 strtolower 和连字符示意
$slug = preg_replace('/[^a-zA-Z0-9]+/u', '-', strtolower($title));
return trim($slug, '-') ?: 'article';
}
private static function makeUniqueSlug(string $slug, int $excludeId): string
{
$original = $slug;
$count = 1;
while (self::where('slug', $slug)->where('id', '', $excludeId)->count() > 0) {
$slug = $original . '-' . $count++;
}
return $slug;
}
}
通过模型事件,我们保证了无论何时文章标题更改,Slug 都会自动更新且保持唯一,业务代码中完全不用关心该细节。
服务层与依赖注入
之前我们已经定义了ArticleService接口和实现。另外,我们可以添加一个TagService作为示例,展示如何在控制器中注入多个服务。但为了简洁,本案例专注于文章服务。实际注入时,直接在构造函数参数列表中添加类型即可。
注解路由控制器编写
创建控制器app/controller/Article.php,使用注解路由定义完整的 RESTful 端点:
// app/controller/Article.php
namespace appcontroller;
use thinkannotationRoute;
use thinkRequest;
use appserviceArticleService;
class Article
{
protected ArticleService $articleService;
public function __construct(ArticleService $articleService)
{
$this->articleService = $articleService;
}
// 文章列表
#[Route('GET', '/api/articles')]
public function index(Request $request)
{
$page = (int) $request->param('page', 1);
$limit = (int) $request->param('limit', 15);
$list = $this->articleService->getList($page, $limit);
return json([
'code' => 200,
'data' => $list,
'msg' => 'success'
]);
}
// 文章详情
#[Route('GET', '/api/articles/<id>')]
public function read(int $id)
{
$article = $this->articleService->getById($id);
if (!$article) {
return json(['code' => 404, 'msg' => '文章不存在'])->code(404);
}
return json(['code' => 200, 'data' => $article]);
}
// 创建文章
#[Route('POST', '/api/articles')]
public function save(Request $request)
{
$data = $request->only(['title', 'content', 'status']);
// 可在此处进行验证(如使用Validate类)
if (empty($data['title']) || empty($data['content'])) {
return json(['code' => 422, 'msg' => '标题和内容不能为空'])->code(422);
}
$id = $this->articleService->create($data);
return json(['code' => 201, 'data' => ['id' => $id], 'msg' => '创建成功'])->code(201);
}
// 更新文章
#[Route('PUT', '/api/articles/<id>')]
public function update(int $id, Request $request)
{
$data = $request->only(['title', 'content', 'status']);
if (empty($data)) {
return json(['code' => 422, 'msg' => '无更新数据'])->code(422);
}
$success = $this->articleService->updateById($id, $data);
if (!$success) {
return json(['code' => 404, 'msg' => '文章不存在或更新失败'])->code(404);
}
return json(['code' => 200, 'msg' => '更新成功']);
}
// 删除文章
#[Route('DELETE', '/api/articles/<id>')]
public function delete(int $id)
{
$success = $this->articleService->deleteById($id);
if (!$success) {
return json(['code' => 404, 'msg' => '文章不存在'])->code(404);
}
return json(['code' => 200, 'msg' => '删除成功']);
}
}
注意,所有路由规则直接通过#[Route]定义,无需在route/app.php中单独配置。依赖注入通过构造函数自动完成,控制器方法中不再有new关键字。
结合中间件实现权限验证
一个真正的 API 往往需要对部分接口进行身份验证。我们可以在注解路由中直接指定中间件。首先创建一个简单的认证中间件:
// app/middleware/Auth.php
namespace appmiddleware;
class Auth
{
public function handle($request, Closure $next)
{
$token = $request->header('Authorization');
if (empty($token)) {
return json(['code' => 401, 'msg' => '未授权'])->code(401);
}
// 验证token... 此处省略具体逻辑
return $next($request);
}
}
然后在控制器类或方法上应用中间件注解。例如,保护所有写操作:
// app/controller/Article.php 局部
use thinkannotationmiddlewareAuth;
class Article
{
// ...
#[Route('POST', '/api/articles')]
#[Auth]
public function save(Request $request) { ... }
#[Route('PUT', '/api/articles/<id>')]
#[Auth]
public function update(int $id, Request $request) { ... }
#[Route('DELETE', '/api/articles/<id>')]
#[Auth]
public function delete(int $id) { ... }
}
这样,只有携带合法 Token 的请求才能创建、更新或删除文章,而列表和详情接口保持公开。注解方式让中间件的应用清晰且集中,无需在路由文件中分散配置。
测试API端点与优化建议
完成上述代码后,可以通过以下命令启动内置服务器进行测试:
php think run
使用 Postman 或 curl 测试接口:
# 获取文章列表
curl http://localhost:8000/api/articles
# 创建文章
curl -X POST http://localhost:8000/api/articles
-H "Content-Type: application/json"
-H "Authorization: Bearer your-token"
-d '{"title":"ThinkPHP 8 注解路由","content":"内容..."}'
# 更新文章
curl -X PUT http://localhost:8000/api/articles/1
-H "Content-Type: application/json"
-H "Authorization: Bearer your-token"
-d '{"title":"新标题"}'
# 删除文章
curl -X DELETE http://localhost:8000/api/articles/1
-H "Authorization: Bearer your-token"
优化建议:
- 请求验证:将控制器中的验证逻辑抽取到独立的 Validate 类中,利用
#[Validate]注解自动验证参数。 - 响应统一:创建自定义 Response 类或使用框架的
json()助手统一返回格式,避免重复编写状态码。 - 事件与缓存:在模型事件中增加缓存清理逻辑(例如发布文章时清除首页缓存),可使用事件监听或模型事件。
- API版本:通过注解路由前缀轻松实现版本控制,例如
#[Route('GET', '/v1/articles')]。
总结
通过本文的实战,你已掌握了 ThinkPHP 8 的注解路由和自动依赖注入的核心用法,并成功构建了一个结构清晰、易于扩展的 RESTful API 系统。注解路由让接口定义更加直观,依赖注入让代码解耦且可测试,模型事件则进一步减少了样板代码。将这三者结合,能显著提升后端开发的效率和质量。现在,不妨在你的下一个项目中全面采用这些现代 PHP 实践,亲身体验它们带来的变革。

