db = Database::connection(); } public function latest(int $limit = 3): array { $statement = $this->db->prepare( "SELECT posts.id, posts.title, posts.slug, posts.category, posts.excerpt, posts.published_at, (SELECT file_path FROM post_images WHERE post_id = posts.id AND caption NOT LIKE '@gtsit:inline:%' ORDER BY sort_order, id LIMIT 1) AS image_path, (SELECT alt_text FROM post_images WHERE post_id = posts.id AND caption NOT LIKE '@gtsit:inline:%' ORDER BY sort_order, id LIMIT 1) AS image_alt FROM posts WHERE posts.status = 'published' AND posts.deleted_at IS NULL AND posts.published_at <= NOW() ORDER BY published_at DESC, id DESC LIMIT :limit" ); $statement->bindValue(':limit', $limit, PDO::PARAM_INT); $statement->execute(); return $statement->fetchAll(); } public function paginate(int $page, int $perPage, ?string $category = null): array { $where = "posts.status = 'published' AND posts.deleted_at IS NULL AND posts.published_at <= NOW()"; $params = []; if ($category !== null && $category !== '') { $where .= ' AND posts.category = :category'; $params['category'] = $category; } $count = $this->db->prepare("SELECT COUNT(*) FROM posts WHERE {$where}"); $count->execute($params); $total = (int) $count->fetchColumn(); $offset = max(0, ($page - 1) * $perPage); $query = $this->db->prepare( "SELECT posts.id, posts.title, posts.slug, posts.category, posts.excerpt, posts.published_at, (SELECT file_path FROM post_images WHERE post_id = posts.id AND caption NOT LIKE '@gtsit:inline:%' ORDER BY sort_order, id LIMIT 1) AS image_path, (SELECT alt_text FROM post_images WHERE post_id = posts.id AND caption NOT LIKE '@gtsit:inline:%' ORDER BY sort_order, id LIMIT 1) AS image_alt FROM posts WHERE {$where} ORDER BY published_at DESC, id DESC LIMIT :limit OFFSET :offset" ); foreach ($params as $key => $value) { $query->bindValue(':' . $key, $value); } $query->bindValue(':limit', $perPage, PDO::PARAM_INT); $query->bindValue(':offset', $offset, PDO::PARAM_INT); $query->execute(); return ['items' => $query->fetchAll(), 'total' => $total]; } public function findPublishedBySlug(string $slug): ?array { $statement = $this->db->prepare( "SELECT posts.id, posts.title, posts.slug, posts.category, posts.excerpt, posts.body, posts.author, posts.published_at, (SELECT file_path FROM post_images WHERE post_id = posts.id AND caption NOT LIKE '@gtsit:inline:%' ORDER BY sort_order, id LIMIT 1) AS image_path, (SELECT alt_text FROM post_images WHERE post_id = posts.id AND caption NOT LIKE '@gtsit:inline:%' ORDER BY sort_order, id LIMIT 1) AS image_alt FROM posts WHERE posts.slug = :slug AND posts.status = 'published' AND posts.deleted_at IS NULL AND posts.published_at <= NOW() LIMIT 1" ); $statement->execute(['slug' => $slug]); $post = $statement->fetch(); return $post ? $this->withMedia($post) : null; } public function withMedia(array $post): array { $statement = $this->db->prepare( 'SELECT id, file_path, alt_text, caption, sort_order FROM post_images WHERE post_id = :post_id ORDER BY sort_order, id' ); $statement->execute(['post_id' => $post['id']]); $post['gallery_images'] = []; $post['inline_images'] = []; foreach ($statement->fetchAll() as $image) { if (preg_match('/^@gtsit:inline:(\d+)$/', (string) $image['caption'], $matches) === 1) { $image['token'] = (int) $matches[1]; $post['inline_images'][(int) $matches[1]] = $image; continue; } $post['gallery_images'][] = $image; } return $post; } }