Un système de commentaires en fil a une table qui se référence elle-même. Chaque commentaire connaît son parent_id (null pour le niveau supérieur), sa depth (base 0), et son status. Les réponses s'imbriquent dans leur parent dans l'arbre de réponse.
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 est dénormalisé dans la ligne pour éviter les requêtes ancestrales récursives à chaque insertion.
La suppression douce remplace le body par [deleted] et définit status = 'deleted'. Les enfants sont préservés :
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
La récupération d'arbre retourne les commentaires supprimés avec des bodies [deleted] pour que la structure du fil reste cohérente — les lecteurs voient un placeholder où était le commentaire supprimé, et ses enfants sont toujours visibles.
Tenter de répondre à un commentaire supprimé retourne 409 :
php
if ($parent->isDeleted()) { return $this->problems->create($request, 'conflict', 'Cannot reply to a deleted comment.', 409, '');}
Charger tous les commentaires pour un post en une seule requête ordonnée par ID (les parents ont toujours des IDs inférieurs à leurs enfants). Puis assembler l'arbre en PHP en deux passes :
php
// Passe 1 : construire la carte de lignes brutes et la liste d'adjacence d'IDs enfantsforeach ($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; }}// Passe 2 : construire récursivement les value objects Comment depuis les racinesreturn $this->buildTree($roots, $rowMap, $childIds);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Garder les lignes brutes et les listes d'IDs enfants int[] séparées des value objects Comment évite la confusion de type PHPStan quand on travaille avec des classes readonly.
Lors de l'assemblage récursif d'un arbre de value objects readonly, PHPStan a besoin de frontières de type propres. Le pattern qui fonctionne :
Passe 1 — construire array<int, array<string, mixed>> $rowMap et array<int, int[]> $childIds depuis les lignes brutes. Pas encore de value objects.
Passe 2 — buildTree() prend uniquement des IDs int[] et les deux cartes, récurse, et hydrate des objets Comment avec des tableaux d'enfants entièrement assemblés.
Cela évite de mélanger des objets Comment et des IDs int dans le même tableau, ce qui causerait un type union que PHPStan ne peut pas réduire.
Commentaires en fil
Implémenter des fils de commentaires auto-référencés avec limites de profondeur et suppression douce.
Vue d'ensemble
Un système de commentaires en fil a une table qui se référence elle-même. Chaque commentaire connaît son
parent_id(null pour le niveau supérieur), sadepth(base 0), et sonstatus. Les réponses s'imbriquent dans leur parent dans l'arbre de réponse.Schéma de base de données
2
3
4
5
6
7
8
9
10
11
12
depthest dénormalisé dans la ligne pour éviter les requêtes ancestrales récursives à chaque insertion.Profondeur maximale
Imposer une limite de profondeur au moment de l'écriture :
2
3
4
5
6
Dans le handler de route, vérifier avant d'insérer :
2
3
4
5
6
7
Suppression douce
La suppression douce remplace le body par
[deleted]et définitstatus = 'deleted'. Les enfants sont préservés :2
3
4
5
6
7
La récupération d'arbre retourne les commentaires supprimés avec des bodies
[deleted]pour que la structure du fil reste cohérente — les lecteurs voient un placeholder où était le commentaire supprimé, et ses enfants sont toujours visibles.Tenter de répondre à un commentaire supprimé retourne 409 :
2
3
Construire l'arbre de commentaires sans N+1
Charger tous les commentaires pour un post en une seule requête ordonnée par ID (les parents ont toujours des IDs inférieurs à leurs enfants). Puis assembler l'arbre en PHP en deux passes :
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Garder les lignes brutes et les listes d'IDs enfants
int[]séparées des value objectsCommentévite la confusion de type PHPStan quand on travaille avec des classesreadonly.Séparer les données de lignes des value objects
Lors de l'assemblage récursif d'un arbre de value objects readonly, PHPStan a besoin de frontières de type propres. Le pattern qui fonctionne :
array<int, array<string, mixed>> $rowMapetarray<int, int[]> $childIdsdepuis les lignes brutes. Pas encore de value objects.buildTree()prend uniquement des IDsint[]et les deux cartes, récurse, et hydrate des objetsCommentavec des tableaux d'enfants entièrement assemblés.Cela évite de mélanger des objets
Commentet des IDsintdans le même tableau, ce qui causerait un type union que PHPStan ne peut pas réduire.Résumé des routes
POST/postsGET/posts/{id}POST/posts/{id}/commentsGET/posts/{id}/commentsPOST/comments/{id}/repliesDELETE/comments/{id}Notes de conception
depthest stocké dans la ligne (dénormalisé) pour éviter les requêtes ancestrales récursives à chaque insertion.ORDER BY id ASCgarantit que les parents apparaissent avant leurs enfants lors du chargement de la liste plate.