exec('CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, excerpt TEXT NOT NULL, body_markdown TEXT NOT NULL, body_html TEXT NOT NULL, cover_media_id INTEGER DEFAULT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY (cover_media_id) REFERENCES media(id) ON DELETE SET NULL )'); $db->exec('CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at DESC)'); } public static function emptyForm(): array { return [ 'title' => '', 'excerpt' => '', 'cover_media_id' => '', 'body_markdown' => '', ]; } public function paginateList(int $page, int $perPage, Media $media): array { $result = $this->paginate( max(0, $page - 1), $perPage, null, ['order' => 'created_at DESC, id DESC'] ); $posts = array_map(fn (self $p): array => $this->summaryRow($p->cast()), $result['subset']); $coverIds = array_filter(array_unique(array_column($posts, 'cover_media_id'))); $covers = $media->findByIds($coverIds); foreach ($posts as &$post) { $cover = $covers[$post['cover_media_id']] ?? null; $post['cover_url'] = $cover['url'] ?? ''; $post['cover_alt'] = $cover['alt'] ?? ''; } return [ 'posts' => $posts, 'page' => max(1, min($page, $result['count'] ?: 1)), 'pages' => $result['count'] ?: 1, ]; } public function findBySlug(string $slug, Media $media): ?array { $this->load(['slug = ?', $slug]); if ($this->dry()) { return null; } $post = $this->summaryRow($this->cast()); $cover = $post['cover_media_id'] > 0 ? $media->findById($post['cover_media_id']) : null; $post['cover_url'] = $cover['url'] ?? ''; $post['cover_alt'] = $cover['alt'] ?? ''; $post['body_html'] = (string) $this->body_html; return $post; } public function findForEdit(int $id): ?array { if ($id <= 0) { return null; } $this->load(['id = ?', $id]); if ($this->dry()) { return null; } return [ 'id' => (int) $this->get('id'), 'title' => (string) $this->title, 'excerpt' => (string) $this->excerpt, 'body_markdown' => (string) $this->body_markdown, 'cover_media_id' => $this->cover_media_id !== null ? (string) ((int) $this->cover_media_id) : '', ]; } public function create(array $input, Media $media): int { $payload = $this->payload($input, $media); $slug = app_unique_slug($payload['title'], fn (string $candidate): bool => $this->count(['slug = ?', $candidate]) > 0); $now = app_now(); $this->reset(); $this->copyfrom($payload + [ 'slug' => $slug, 'created_at' => $now, 'updated_at' => $now, ]); $this->save(); return (int) $this->get('id'); } public function updatePost(int $id, array $input, Media $media): bool { $this->load(['id = ?', $id]); if ($this->dry()) { return false; } $payload = $this->payload($input, $media); $this->copyfrom($payload + ['updated_at' => app_now()]); $this->save(); return true; } // Vérifie les deux usages possibles : couverture (cover_media_id) // et images insérées dans le corps (media:filename dans body_markdown). public function isMediaUsed(int $mediaId, string $fileName): bool { return $this->count([ 'cover_media_id = ? OR body_markdown LIKE ?', $mediaId, '%media:' . $fileName . '%', ]) > 0; } public function delete(int $id): void { $this->load(['id = ?', $id]); if ($this->dry()) { throw new RuntimeException('Article introuvable.'); } $this->erase(); } private function payload(array $input, Media $media): array { $title = trim((string) ($input['title'] ?? '')); $excerpt = trim((string) ($input['excerpt'] ?? '')); $bodyMarkdown = trim((string) ($input['body_markdown'] ?? '')); $coverMediaId = trim((string) ($input['cover_media_id'] ?? '')); if ($title === '') { throw new RuntimeException('Ajoute un titre.'); } if (mb_strlen($title) > self::TITLE_MAX_LENGTH) { throw new RuntimeException('Le titre est trop long.'); } if ($excerpt === '') { throw new RuntimeException('Ajoute un extrait.'); } if (mb_strlen($excerpt) > self::EXCERPT_MAX_LENGTH) { throw new RuntimeException("L'extrait est trop long."); } $coverId = null; if ($coverMediaId !== '') { $coverId = (int) $coverMediaId; if ($media->findById($coverId) === null) { throw new RuntimeException('Image de couverture introuvable.'); } } $bodyHtml = MarkdownService::instance()->compile($bodyMarkdown, $media); return [ 'title' => $title, 'excerpt' => $excerpt, 'body_markdown' => $bodyMarkdown, 'body_html' => $bodyHtml, 'cover_media_id' => $coverId, ]; } private function summaryRow(array $row): array { return [ 'id' => (int) $row['id'], 'title' => (string) $row['title'], 'slug' => (string) $row['slug'], 'excerpt' => (string) $row['excerpt'], 'cover_media_id' => (int) ($row['cover_media_id'] ?? 0), 'created_at' => (string) $row['created_at'], 'updated_at' => (string) $row['updated_at'], ]; } }