CREATE TABLE comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, post_id INTEGER NOT NULL, parent_id INTEGER, author_name TEXT NOT NULL, body TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'published', depth INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, FOREIGN KEY (post_id) REFERENCES posts(id), FOREIGN KEY (parent_id) REFERENCES comments(id));
软删除将 body 替换为 [deleted] 并设置 status = 'deleted'。子评论得以保留:
php
public function softDelete(int $id): void{ $this->executor->execute( "UPDATE comments SET status = 'deleted', body = '[deleted]' WHERE id = ?", [$id], );}
1 2 3 4 5 6 7
树形获取返回带 [deleted] body 的已删除评论,使线程结构保持连贯——读者看到删除评论所在位置的占位符,其子评论仍然可见。
尝试回复已删除评论返回 409:
php
if ($parent->isDeleted()) { return $this->problems->create($request, 'conflict', 'Cannot reply to a deleted comment.', 409, '');}
嵌套评论
实现带深度限制和软删除的自引用评论线程。
概述
嵌套评论系统包含一张自引用的表。每条评论都知道自己的
parent_id(顶层为 null)、depth(从 0 开始)和status。在响应树中,回复嵌套在其父评论内部。数据库结构
2
3
4
5
6
7
8
9
10
11
12
depth被反规范化存储到行中,以避免每次插入时进行递归祖先查询。最大深度
在写入时强制深度限制:
2
3
4
5
6
在路由处理器中,插入前进行检查:
2
3
4
5
6
7
软删除
软删除将 body 替换为
[deleted]并设置status = 'deleted'。子评论得以保留:2
3
4
5
6
7
树形获取返回带
[deleted]body 的已删除评论,使线程结构保持连贯——读者看到删除评论所在位置的占位符,其子评论仍然可见。尝试回复已删除评论返回 409:
2
3
无 N+1 问题地构建评论树
通过单次按 ID 排序的查询加载文章的所有评论(父评论的 ID 始终小于子评论)。然后在 PHP 中通过两次遍历组装树:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
将原始行和
int[]子 ID 列表与Comment值对象分离,可以在使用readonly类时避免 PHPStan 类型混淆。将行数据与值对象分离
在递归组装 readonly 值对象树时,PHPStan 需要清晰的类型边界。有效的模式:
array<int, array<string, mixed>> $rowMap和array<int, int[]> $childIds。还不创建值对象。buildTree()只接受int[]ID 和两个映射,递归地将Comment对象与完整组装的子数组进行水合。这避免了在同一个数组中混合
Comment对象和intID,否则会产生 PHPStan 无法收窄的联合类型。路由概览
POST/postsGET/posts/{id}POST/posts/{id}/commentsGET/posts/{id}/commentsPOST/comments/{id}/repliesDELETE/comments/{id}设计说明
depth存储在行中(反规范化),以避免每次插入时进行递归祖先查询。ORDER BY id ASC保证在加载扁平列表时父评论出现在子评论之前。