ThinkPHP可通过自定义模型与手动关联实现多态收藏功能:建favorites表含favorable_id/type字段,Favorite模型中用getFavorable()动态查目标实例,各资源模型添加where限定type的反向关联,控制器校验类型白名单后操作收藏。

ThinkPHP 本身不直接支持“多态关联”(Polymorphic Relationship)这种 Laravel 风格的数据库设计,但可以通过合理建模 + 手动关联逻辑,在 ThinkPHP 6/7 中完整实现「一个收藏表,支持收藏文章、视频、商品等多种模型」的多态收藏功能。关键在于:统一收藏表结构 + 动态解析目标模型 + 手动构造关联查询。
1. 设计多态收藏数据表
创建一张 favorites 表,核心字段如下:
- id:主键
- user_id:收藏用户 ID(关联 think_user)
- favorable_id:被收藏记录的 ID(如 article.id 或 video.id)
-
favorable_type:被收藏模型的类名或标识(推荐用完整类名,如
app\model\Article) - created_at:收藏时间
✅ 示例 SQL(ThinkPHP 迁移写法或手动建表):
CREATE TABLE `favorites` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `user_id` bigint unsigned NOT NULL, `favorable_id` bigint unsigned NOT NULL, `favorable_type` varchar(100) NOT NULL COMMENT '模型类全名,如 app\model\Article', `created_at` int(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `user_type_id` (`user_id`,`favorable_type`,`favorable_id`) );
2. 编写 Favorite 模型并封装多态方法
在 app\model\Favorite.php 中定义模型,并添加动态获取被收藏对象的方法:
立即学习“PHP免费学习笔记(深入)”;
namespace app\model;
use think\Model;
class Favorite extends Model
{
// 自动写入时间戳(若用 int 时间戳)
protected $autoWriteTimestamp = true;
protected $createTime = 'created_at';
protected $updateTime = false;
// 获取被收藏的目标模型实例(多态核心)
public function getFavorable()
{
$type = $this->favorable_type;
$id = $this->favorable_id;
if (class_exists($type)) {
return $type::find($id);
}
return null;
}
}
? 小技巧:可在模型中加个 isFavoritedBy($userId) 静态方法,方便快速判断某用户是否已收藏某资源。
3. 在目标模型(如 Article/Video)中添加“被谁收藏了”反向关联
以 app\model\Article.php 为例,添加一个获取收藏用户的关联方法:
namespace app\model;
use think\Model;
class Article extends Model
{
// 反向:查出哪些用户收藏了这篇文章
public function favoritedUsers()
{
return $this->belongsToMany(
User::class, // 关联用户模型
'favorites', // 中间表名
'favorable_id', // 中间表外键(指向本模型)
'user_id', // 中间表外键(指向用户)
'id', // 本模型主键
'id' // 用户模型主键
)
->where('favorable_type', self::class); // ✅ 关键:限定类型
}
}
调用方式:$article->favoritedUsers()->select() 即可拿到收藏该文章的用户列表。
4. 收藏/取消收藏接口(控制器示例)
在 app\controller\FavoriteController.php 中实现核心操作:
namespace app\controller;
use app\model\Favorite;
use app\model\Article;
use think\Request;
class FavoriteController
{
public function store(Request $request)
{
$userId = $request->userId(); // 假设已通过中间件注入登录用户ID
$type = $request->param('type'); // 如 'article', 'video'
$id = $request->param('id/d', 0);
// 映射 type → 模型类(避免前端传任意类名,防漏洞)
$map = [
'article' => \app\model\Article::class,
'video' => \app\model\Video::class,
'product' => \app\model\Product::class,
];
$modelClass = $map[$type] ?? null;
if (!$modelClass || !$modelClass::find($id)) {
return json(['code' => 400, 'msg' => '资源不存在']);
}
// 防重复收藏
$exists = Favorite::where([
'user_id' => $userId,
'favorable_id' => $id,
'favorable_type' => $modelClass,
])->count();
if ($exists) {
return json(['code' => 400, 'msg' => '已收藏']);
}
Favorite::create([
'user_id' => $userId,
'favorable_id' => $id,
'favorable_type' => $modelClass,
]);
return json(['code' => 200, 'msg' => '收藏成功']);
}
public function destroy($id)
{
$userId = request()->userId();
Favorite::where([
'user_id' => $userId,
'id' => $id,
])->delete();
return json(['code' => 200, 'msg' => '已取消收藏']);
}
}
⚠️ 注意:生产环境建议用 favorable_type 字段做白名单校验,禁止用户直接传类名字符串。
5. 前端调用与分页展示收藏列表
获取当前用户所有收藏(含不同类型混排):
// 控制器中
$favorites = Favorite::with('favorable') // 触发 getFavorable() 方法
->where('user_id', $userId)
->order('created_at', 'desc')
->paginate(15);
// 返回给前端的数据结构类似:
// [
// { id: 1, favorable_id: 101, favorable_type: 'app\model\Article', favorable: { id: 101, title: 'TP6 多态实践' } },
// { id: 2, favorable_id: 205, favorable_type: 'app\model\Video', favorable: { id: 205, title: '收藏功能演示' } }
// ]
前端可根据 favorable_type 渲染不同卡片样式,或跳转到对应详情页(如 /article/101 或 /video/205)。
不复杂但容易忽略:多态本质是「用两个字段模拟一对多+类型路由」,ThinkPHP 虽无原生语法糖,但靠模型方法 + 显式条件完全可落地,且更可控、易调试。



















