(int) $p['id']], ); if ($roots === []) { Response::json(['data' => []]); return; } // 1 requête en plus pour toutes les réponses, regroupées par parent_id. $rootIds = array_map(static fn(array $r): int => (int) $r['id'], $roots); $in = implode(',', array_fill(0, count($rootIds), '?')); $replies = Database::fetchAll( "SELECT id, project_id, parent_id, author_name, author_member_id, body, created_at, updated_at FROM discussions WHERE parent_id IN ($in) ORDER BY created_at ASC, id ASC", $rootIds, ); $byParent = []; foreach ($replies as $r) { $byParent[(int) $r['parent_id']][] = self::present($r); } $tree = []; foreach ($roots as $r) { $node = self::present($r); $node['replies'] = $byParent[(int) $r['id']] ?? []; $tree[] = $node; } Response::json(['data' => $tree]); } public function store(Request $req, array $params): void { $p = ProjectsController::findOr404($params['slug']); $body = $req->json(); $v = new Validator($body); $v->required('author_name')->string('author_name', 128); $v->required('body')->string('body'); $v->integer('parent_id', 1); $v->integer('author_member_id', 1); $v->check(); $parentId = null; if (isset($body['parent_id'])) { $parent = Database::fetchOne( 'SELECT id, project_id FROM discussions WHERE id = :id', [':id' => (int) $body['parent_id']], ); if ($parent === null || (int) $parent['project_id'] !== (int) $p['id']) { $v->setError('parent_id', 'not_found'); $v->check(); } $parentId = (int) $parent['id']; } $authorMemberId = null; if (isset($body['author_member_id'])) { $exists = Database::fetchValue( 'SELECT 1 FROM members WHERE id = :id', [':id' => (int) $body['author_member_id']], ); if ($exists === null) { $v->setError('author_member_id', 'not_found'); $v->check(); } $authorMemberId = (int) $body['author_member_id']; } $id = Database::insert('discussions', [ 'project_id' => (int) $p['id'], 'parent_id' => $parentId, 'author_name' => (string) $body['author_name'], 'author_member_id' => $authorMemberId, 'body' => (string) $body['body'], ]); ActivityLogger::log( 'posted_discussion', sprintf('a publié un message dans « %s »', $p['title']), (int) $p['id'], $authorMemberId, (string) $body['author_name'], ); $row = Database::fetchOne( 'SELECT * FROM discussions WHERE id = :id', [':id' => $id], ); Response::json(self::present($row), 201); } public function update(Request $req, array $params): void { $p = ProjectsController::findOr404($params['slug']); $msg = self::findMsgOr404((int) $params['id'], (int) $p['id']); $body = $req->json(); $v = new Validator($body); $v->string('author_name', 128); $v->string('body'); $v->check(); $update = []; foreach (['author_name', 'body'] as $f) { if (array_key_exists($f, $body)) { $update[$f] = $body[$f]; } } if ($update !== []) { Database::update('discussions', $update, 'id = :id', [':id' => (int) $msg['id']]); } $fresh = Database::fetchOne('SELECT * FROM discussions WHERE id = :id', [':id' => (int) $msg['id']]); Response::json(self::present($fresh)); } public function destroy(Request $req, array $params): void { $p = ProjectsController::findOr404($params['slug']); $msg = self::findMsgOr404((int) $params['id'], (int) $p['id']); // FK parent_id ON DELETE CASCADE -> les réponses partent avec. Database::delete('discussions', 'id = :id', [':id' => (int) $msg['id']]); Response::noContent(); } private static function findMsgOr404(int $id, int $projectId): array { $row = Database::fetchOne( 'SELECT * FROM discussions WHERE id = :id AND project_id = :pid', [':id' => $id, ':pid' => $projectId], ); if ($row === null) { throw new \ApiException(404, ['error' => 'not_found']); } return $row; } private static function present(array $row): array { return [ 'id' => (int) $row['id'], 'project_id' => (int) $row['project_id'], 'parent_id' => $row['parent_id'] === null ? null : (int) $row['parent_id'], 'author_name' => $row['author_name'], 'author_member_id' => $row['author_member_id'] === null ? null : (int) $row['author_member_id'], 'body' => $row['body'], 'created_at' => $row['created_at'], 'updated_at' => $row['updated_at'], ]; } }