Um sistema de comentários em thread tem uma tabela que referencia a si mesma. Cada comentário conhece seu parent_id (null para nível superior), sua depth (baseado em 0) e seu status. As respostas se aninham dentro de seu pai na árvore de resposta.
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));
1 2 3 4 5 6 7 8 9 10 11 12
depth é desnormalizado na linha para evitar queries de ancestrais recursivas em cada inserção.
Soft delete substitui o corpo por [deleted] e define status = 'deleted'. Os filhos são preservados:
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
A busca da árvore retorna comentários excluídos com corpos [deleted] para que a estrutura do thread permaneça coerente — os leitores veem um placeholder onde o comentário excluído estava, e seus filhos ainda são visíveis.
Tentar responder a um comentário excluído retorna 409:
php
if ($parent->isDeleted()) { return $this->problems->create($request, 'conflict', 'Não é possível responder a um comentário excluído.', 409, '');}
Carregue todos os comentários de uma postagem em uma única query ordenada por ID (pais sempre têm IDs menores que seus filhos). Então monte a árvore em PHP usando dois passos:
php
// Passo 1: construir mapa de linhas brutas e lista de adjacência de IDs de filhosforeach ($rows as $row) { $rowMap[(int) $row['id']] = $row; $childIds[(int) $row['id']] = [];}foreach ($rowMap as $id => $row) { $parentId = isset($row['parent_id']) ? (int) $row['parent_id'] : null; if ($parentId === null) { $roots[] = $id; } elseif (isset($childIds[$parentId])) { $childIds[$parentId][] = $id; }}// Passo 2: construir recursivamente objetos Comment a partir das raízesreturn $this->buildTree($roots, $rowMap, $childIds);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Manter linhas brutas e listas int[] de IDs de filhos separadas dos value objects Comment evita confusão de tipos no PHPStan ao trabalhar com classes readonly.
Comentários em Thread
Implemente threads de comentários com auto-referência com limites de profundidade e soft delete.
Visão Geral
Um sistema de comentários em thread tem uma tabela que referencia a si mesma. Cada comentário conhece seu
parent_id(null para nível superior), suadepth(baseado em 0) e seustatus. As respostas se aninham dentro de seu pai na árvore de resposta.Schema do Banco de Dados
2
3
4
5
6
7
8
9
10
11
12
depthé desnormalizado na linha para evitar queries de ancestrais recursivas em cada inserção.Profundidade Máxima
Aplique um limite de profundidade no momento da escrita:
2
3
4
5
6
No handler de rota, verifique antes de inserir:
2
3
4
5
6
7
Soft Delete
Soft delete substitui o corpo por
[deleted]e definestatus = 'deleted'. Os filhos são preservados:2
3
4
5
6
7
A busca da árvore retorna comentários excluídos com corpos
[deleted]para que a estrutura do thread permaneça coerente — os leitores veem um placeholder onde o comentário excluído estava, e seus filhos ainda são visíveis.Tentar responder a um comentário excluído retorna 409:
2
3
Construindo a Árvore de Comentários Sem N+1
Carregue todos os comentários de uma postagem em uma única query ordenada por ID (pais sempre têm IDs menores que seus filhos). Então monte a árvore em PHP usando dois passos:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Manter linhas brutas e listas
int[]de IDs de filhos separadas dos value objectsCommentevita confusão de tipos no PHPStan ao trabalhar com classesreadonly.Separando Dados de Linha de Value Objects
Ao montar uma árvore de value objects readonly recursivamente, o PHPStan precisa de fronteiras de tipo limpas. O padrão que funciona:
array<int, array<string, mixed>> $rowMapearray<int, int[]> $childIdsa partir de linhas brutas. Sem value objects ainda.buildTree()recebe apenas IDsint[]e os dois mapas, recursa e hidrata objetosCommentcom arrays de filhos completamente montados.Isso evita misturar objetos
Commente IDsintno mesmo array, o que causaria um tipo union que o PHPStan não consegue restringir.Resumo das Rotas
POST/postsGET/posts/{id}POST/posts/{id}/commentsGET/posts/{id}/commentsPOST/comments/{id}/repliesDELETE/comments/{id}Notas de Design
depthé armazenado na linha (desnormalizado) para evitar queries de ancestrais recursivas em cada inserção.ORDER BY id ASCgarante que pais apareçam antes de seus filhos ao carregar a lista plana.