213 lines
6.3 KiB
PHP
213 lines
6.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
class Post extends DB\SQL\Mapper
|
|
{
|
|
public const TITLE_MAX_LENGTH = 120;
|
|
public const EXCERPT_MAX_LENGTH = 240;
|
|
|
|
public function __construct()
|
|
{
|
|
parent::__construct(Base::instance()->get('DB'), 'posts');
|
|
}
|
|
|
|
public static function bootstrap(DB\SQL $db): void
|
|
{
|
|
if ($db->schema('posts', null, 0)) {
|
|
return;
|
|
}
|
|
|
|
$db->exec('CREATE TABLE 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 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'],
|
|
];
|
|
}
|
|
|
|
}
|