Less home code more F3

This commit is contained in:
julien
2026-03-30 00:00:03 +02:00
parent d71cf304a9
commit fac7f60190
30 changed files with 818 additions and 1552 deletions

View File

@@ -25,129 +25,87 @@ class Post extends DB\SQL\Mapper
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
updated_at TEXT NOT NULL
)');
$db->exec('CREATE INDEX idx_posts_created_at ON posts(created_at DESC)');
}
public static function emptyForm(): array
public static function blank(): array
{
return [
'title' => '',
'excerpt' => '',
'cover_media_id' => '',
'body_markdown' => '',
];
}
public function paginateList(int $page, int $perPage, Media $media): array
public function page(int $page, int $perPage): 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'] ?? '';
}
$result = $this->paginate(max(0, $page - 1), $perPage, null, ['order' => 'created_at DESC, id DESC']);
$items = array_map(fn(self $row): array => $this->summary($row->cast()), $result['subset'] ?: []);
return [
'posts' => $posts,
'page' => max(1, min($page, $result['count'] ?: 1)),
'pages' => $result['count'] ?: 1,
'items' => $items,
'pagination' => [
'page' => max(1, min($page, $result['count'] ?: 1)),
'pages' => max(1, (int) ($result['count'] ?: 1)),
],
];
}
public function findBySlug(string $slug, Media $media): ?array
public function findBySlug(string $slug): ?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;
$row = $this->cast();
$post = $this->summary($row) + ['body_html' => (string) $row['body_html']];
return $post;
}
public function findForEdit(int $id): ?array
public function findForForm(int $id): ?array
{
if ($id <= 0) {
return null;
}
$this->load(['id = ?', $id]);
if ($this->dry()) {
return null;
}
return [
'id' => (int) $this->get('id'),
'id' => (int) $this->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
public function savePost(array $input, ?int $id = null): int
{
$payload = $this->payload($input, $media);
$slug = app_unique_slug($payload['title'], fn (string $candidate): bool => $this->count(['slug = ?', $candidate]) > 0);
$payload = $this->payload($input);
$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;
if ($id === null) {
$this->reset();
$payload['slug'] = $this->uniqueSlug($payload['title']);
$payload['created_at'] = $now;
} else {
$this->load(['id = ?', $id]);
if ($this->dry()) {
throw new RuntimeException('Article introuvable.');
}
}
$payload = $this->payload($input, $media);
$this->copyfrom($payload + ['updated_at' => app_now()]);
$payload['updated_at'] = $now;
$this->copyfrom($payload);
$this->save();
return true;
return (int) $this->id;
}
// 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
public function deleteById(int $id): void
{
$this->load(['id = ?', $id]);
if ($this->dry()) {
@@ -157,12 +115,16 @@ class Post extends DB\SQL\Mapper
$this->erase();
}
private function payload(array $input, Media $media): array
public function usesMedia(string $fileName): bool
{
return $this->count(['body_markdown LIKE ?', '%media:' . $fileName . '%']) > 0;
}
private function payload(array $input): array
{
$title = trim((string) ($input['title'] ?? ''));
$excerpt = trim((string) ($input['excerpt'] ?? ''));
$bodyMarkdown = trim((string) ($input['body_markdown'] ?? ''));
$coverMediaId = trim((string) ($input['cover_media_id'] ?? ''));
$body = trim((string) ($input['body_markdown'] ?? ''));
if ($title === '') {
throw new RuntimeException('Ajoute un titre.');
@@ -174,39 +136,67 @@ class Post extends DB\SQL\Mapper
throw new RuntimeException('Ajoute un extrait.');
}
if (mb_strlen($excerpt) > self::EXCERPT_MAX_LENGTH) {
throw new RuntimeException("L'extrait est trop long.");
throw new RuntimeException('Lextrait 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,
'body_markdown' => $body,
'body_html' => MarkdownService::instance()->compile($body, new Media()),
];
}
private function summaryRow(array $row): array
private function uniqueSlug(string $title): string
{
$base = app_slug($title);
$slug = $base;
$n = 2;
while ($this->count(['slug = ?', $slug]) > 0) {
$slug = $base . '-' . $n;
$n++;
}
return $slug;
}
private function summary(array $row): array
{
$thumbnail = $this->firstImage((string) ($row['body_html'] ?? ''));
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),
'thumbnail_url' => $thumbnail['url'],
'thumbnail_alt' => $thumbnail['alt'],
'created_at' => (string) $row['created_at'],
'updated_at' => (string) $row['updated_at'],
];
}
private function firstImage(string $html): array
{
if ($html === '') {
return ['url' => '', 'alt' => ''];
}
if (!preg_match('~(<img\s[^>]*src="([^"]+)"[^>]*>)~i', $html, $match)) {
return ['url' => '', 'alt' => ''];
}
$tag = $match[1];
$alt = '';
if (preg_match('~alt="([^"]*)"~i', $tag, $altMatch)) {
$alt = html_entity_decode($altMatch[1], ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
return [
'url' => html_entity_decode($match[2], ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'alt' => $alt,
];
}
}