ThinkPHP 8 注解路由与模型事件实战:告别路由文件,模型钩子竟然这样玩

2026-08-28 0 636

熟悉 ThinkPHP 的朋友应该知道,以前我们定义一个路由就要在 route 目录下写一行 Route::get(…),控制器一多,路由文件就变得又臭又长。TP8.0 带来了注解路由的完善支持,直接在控制器方法上写一行 @Route 注释,路由就自动绑定。再配合模型事件,很多重复的字段处理也能丢给模型自动完成。今天我就用一个小小的文章发布功能,演示一下这对组合有多爽。

环境准备

你至少需要 ThinkPHP 8.0 或更高版本,并且 PHP 版本要 >= 8.0。建议是 8.1。创建项目我默认你已经会了,直接安装:

composer create-project topthink/think tp8-demo
    

然后开启注解路由支持,在 config/route.php 里把 controller_suffix 设置为 true,并且确保 with_route 是打开的。你也可以完全依赖注解,不用再手动写动态路由。

注解路由怎么写

打开一个控制器,比如 app/controller/Article.php,在方法上面加一行 #[Route('hello')]。这里 TP8 用的是 PHP 原生的注解属性,不是 dockblock。新建一个测试方法:

<?php
namespace appcontroller;

use thinkannotationRoute;

class Article
{
    #[Route('index')]
    public function index()
    {
        return json(['code' => 0, 'msg' => 'ok']);
    }
}
    

注意看,我没有在 route.php 里写任何东西,浏览器访问 /article/index.html 就能访问到。因为 ThinkPHP 的默认路由规则是 控制器/方法,当你用注解时,它其实是在补充一条显式路由。如果你想自定义路径,可以这样:

#[Route('article_list')]
public function list()
{
    return json(['code' => 0, 'msg' => 'list']);
}
    

这样访问 /article_list.html 就会执行这个 list 方法。是不是有点像 Laravel 的。但如果你觉得写注解还是麻烦,TP8 还支持分组注解:

#[RouteGroup('api')]
class Article
{
    #[Route('index')]
    public function index() {}
}
    

这会把类下所有方法的路径前缀加上 api,访问 /api/article/index.html。注意这里有一个坑:控制器类名会变成路由名,所以 index 方法最终路径是 /api/article/index,除非你在方法上用了一个带前缀的路由。其实 TP8 的注解路由已经非常接近 Laravel 的 route 注解了,只是参数写法稍有差异。

真实案例:文章发布

现在做一个小功能:用户提交文章标题和内容,后端写入数据库,写入后自动给标题加工一下,变成“【推荐】标题”。如果用传统写法,你需要在控制器里写很多业务代码。现在我们把控制器变干净,让模型事件来处理。

先创建数据库表 article,字段有 id, title, content, create_time。

CREATE TABLE article (
  id int unsigned auto_increment primary key,
  title varchar(255) not null,
  content text not null,
  create_time int unsigned not null
);
    

创建一个模型

app/model 目录新建 Article.php

<?php
namespace appmodel;

use thinkModel;

class Article extends Model
{
    // 关闭自动时间戳,我们手动处理
    protected $autoWriteTimestamp = false;

    // 模型事件:写入前触发(文章标题自动加推荐前缀)
    public static function onBeforeInsert($article)
    {
        // 如果标题没有“推荐”字样,加上
        if (strpos($article->title, '【推荐】') === false) {
            $article->title = '【推荐】' . $article->title;
        }
    }

    // 模型事件:写入后触发(记录日志,或者做其他事)
    public static function onAfterInsert($article)
    {
        // 可以在这里写日志,比如写入到 log 表
        // 别用 print_r,直接记录到文件或走日志通道
        trace('新文章入库,ID:' . $article->id . ',标题:' . $article->title);
    }
}
    

ThinkPHP 的模型事件有很多,比如 onBeforeInsert、onAfterInsert、onBeforeUpdate,它们会在对应的数据库操作时自动调用。我们这里用 onBeforeInsert 修改标题,onAfterInsert 记录一条日志。

控制器变得非常干净

app/controller/Article.php 中写一个保存方法:

<?php
namespace appcontroller;

use thinkannotationRoute;
use appmodelArticle as ArticleModel;

class Article
{
    #[Route('save', method: 'POST')]
    public function save()
    {
        $article = new ArticleModel();
        $article->title = input('title');
        $article->content = input('content');
        $article->create_time = time();
        $article->save(); // 触发模型事件

        return json(['code' => 0, 'msg' => '发布成功', 'data' => $article]);
    }

    #[Route('read/:id', method: 'GET')]
    public function read($id)
    {
        $article = ArticleModel::find($id);
        if (!$article) {
            return json(['code' => 1, 'msg' => '文章不存在']);
        }
        return json(['code' => 0, 'data' => $article]);
    }
}
    

注意这里我把路由方法改成 POST 和 GET 分开,注解写法是 method: 'POST'。这样前端可以通过 /article/save.html 来 POST 提交。在默认路由下,如果你不写注解,直接用 /article/save.html 也能 POST 到,但是用了注解路由后,它就成了一个显式的路由,并且会限制方法。

完整流程走一遍

我们先用 Postman 模拟 POST 请求,提交 title 和 content,内容随便写“今天学习了 ThinkPHP8”。

POST /article/save.html
title=今日学习随笔
content=今天搞明白了注解路由和模型事件
    

发出去后,返回:

{
    "code": 0,
    "msg": "发布成功",
    "data": {
        "id": 1,
        "title": "【推荐】今日学习随笔",
        "content": "今天搞明白了注解路由和模型事件",
        "create_time": 1732322222
    }
}
    

请注意 title 字段,被模型事件自动加了“【推荐】”前缀,你根本没有在控制器里改这个字符串。这就是模型钩子的价值:当你要给一些字段做统一预处理时,直接在模型里挂事件,而不是在每个控制器里写重复代码。以后哪怕换成命令行脚本,也会自动调用这个事件,万无一失。

onAfterInsert 到底干了啥

上面的 trace 已经写入了日志。你可以打开 runtime/log 目录下最新的日志文件看看,里面会有一条 新文章入库,ID:1,标题:【推荐】今日学习随笔。这就是 onAfterInsert 被触发的结果。实际上你可以在 onAfterInsert 里发通知、写队列、同步到搜索引擎,总之不用弄脏控制器。

把模型事件用到歪门邪道上

比如有的系统需要把文章标题的首字母提取出来作为索引,传统做法是控制器里写一段函数。现在你可以直接内置在模型里:

public static function onBeforeInsert($article)
{
    // 生成标题首字母
    $article->letter = strtoupper(substr($article->title, 0, 1));
    // 如果标题内容包含敏感词,直接抛异常,写入失败
    if (strpos($article->content, '违法') !== false) {
        throw new Exception('内容包含敏感词,禁止发布');
    }
}
    

这里注意,如果你在事件里抛异常,save() 会直接失败,这样可以统一做数据校验。但我不推荐把复杂校验放事件里,还是用验证器更合适。不过简单规则完全可行。

注解路由的另外一些玩法

除了方法级路由,还有资源路由。TP8 支持用 #[Resource('article')] 自动生成 RESTful 方法列表。你可以这样写:

<?php
namespace appcontroller;

use thinkannotationRoute;
use appmodelArticle as ArticleModel;

#[RouteGroup('api')]
class Article
{
    #[Route('resource', method: 'GET')]
    public function index() // 列表
    {
        $list = ArticleModel::order('id desc')->select();
        return json($list);
    }

    #[Route('resource/create', method: 'GET')]
    public function create() {}

    #[Route('resource/:id', method: 'GET')]
    public function read($id) {}

    #[Route('resource/:id/edit', method: 'GET')]
    public function edit($id) {}

    #[Route('resource', method: 'POST')]
    public function save() {}

    #[Route('resource/:id', method: 'PUT')]
    public function update($id) {}

    #[Route('resource/:id', method: 'DELETE')]
    public function delete($id) {}
}
    

然后你就能用标准的 RESTful 路径来访问了。POST /api/article/resource 就是新增,DELETE /api/article/resource/1 是删除。这比手动写一堆路由规则要清晰得多。

踩坑记录

第一,注解路由需要安装 topthink/think-annotation 扩展包。好在 TP8 默认已经装好了。如果你是在旧项目上升级,记得 composer require topthink/think-annotation。

第二,解析注解需要缓存,所以部署后如果改了注解,记得清理 runtime 下的缓存文件,否则注解不生效。

第三,路由匹配顺序问题。如果你用了分组注解,又用了方法上的子路由,相同路径时 TP8 会优先匹配有路由注解的方法。但当你有多个方法都匹配同一个 URL 时,TP8 大概率会报冲突,你必须规范好路径,不要有两个方法访问同一个 URL 但没有唯一路由。

第四,POST 请求时,引入参数需要用 input() 而不是 $_POST,因为 TP8 做了过滤。但如果你用了注解路由,input() 是推荐方式,因为它会自动处理不同 content-type。

模型事件和注解路由合体后的效果

以前写一个文章模块,需要准备:路由文件(至少 7 行)、控制器存数据并处理标题前缀、数据库插入。现在只需要控制器里三行代码,模型帮你做预处理,路由只要一行注解。最大的收益是:当你有好几个地方创建文章(比如后台、微信小程序等),这些入口调用的都是同一个模型,那么所有写入操作都会自动遵循“标题加【推荐】”这个规则。

如果你觉得模型事件还是有点“隐式”不太好 debug,你可以在事件里打断点或者输出到专用日志,但最好不要输出到页面。其实 ThinkPHP 的事件机制就是观察者模式,你习惯了就会觉得很自然。

总结

今天通过一个文章发布功能,把 TP8 注解路由和模型事件串了起来。注解路由能减少路由文件的繁杂,模型事件能让数据逻辑内聚到模型里,而不是控制器。这两者结合,你甚至会感觉到控制器变得十分“空”,但功能一点都没少。这是一种很好的开发体验。

如果你的项目还没有升级到 TP8,强烈建议试一下。在官方文档里,注解路由和模型事件都有详细说明,但很多示例都是割裂的。把它们放在一个真实的场景里,你才能感受到威力。

下次再有人问你怎么用 TP8,你可以直接丢这篇文章给他。如果想继续深入,后面可以聊聊模型搜索器和访问器的配合,那也很有趣。

ThinkPHP 8 注解路由与模型事件实战:告别路由文件,模型钩子竟然这样玩
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 thinkphp ThinkPHP 8 注解路由与模型事件实战:告别路由文件,模型钩子竟然这样玩 https://www.taomawang.com/server/thinkphp/2660.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务