feat : notion de livre — grouper des pages en série ordonnée
Ajoute un concept de "livre" (série de pages ordonnées) avec : - BookManager : CRUD JSON dans data/books/<slug>.json - Route /book/<slug> → page de sommaire (table des matières) - Navigation chapitre ← → en bas de chaque article membre du livre - Bandeau "Chapitre X/N — Nom du livre" en haut de l'article - Admin → onglet Livres : créer, éditer, supprimer un livre, ajouter/ordonner les pages via textarea slug Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class BookManager
|
||||
{
|
||||
public function __construct(private string $booksDir)
|
||||
{
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Lecture
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
$books = [];
|
||||
if (!is_dir($this->booksDir)) {
|
||||
return $books;
|
||||
}
|
||||
foreach (scandir($this->booksDir) as $file) {
|
||||
if (!str_ends_with($file, '.json')) {
|
||||
continue;
|
||||
}
|
||||
$raw = file_get_contents($this->booksDir . '/' . $file);
|
||||
if ($raw === false) {
|
||||
continue;
|
||||
}
|
||||
$book = json_decode($raw, true);
|
||||
if (!is_array($book) || empty($book['slug'])) {
|
||||
continue;
|
||||
}
|
||||
$books[] = $book;
|
||||
}
|
||||
usort($books, static fn ($a, $b) => strcmp($a['title'] ?? '', $b['title'] ?? ''));
|
||||
return $books;
|
||||
}
|
||||
|
||||
public function getBySlug(string $slug): ?array
|
||||
{
|
||||
$path = $this->bookPath($slug);
|
||||
if (!file_exists($path)) {
|
||||
return null;
|
||||
}
|
||||
$raw = file_get_contents($path);
|
||||
if ($raw === false) {
|
||||
return null;
|
||||
}
|
||||
$book = json_decode($raw, true);
|
||||
return is_array($book) && !empty($book['slug']) ? $book : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cherche dans quel livre se trouve un article (par son slug).
|
||||
* Retourne le contexte complet ou null si l'article n'appartient à aucun livre.
|
||||
*
|
||||
* @return array{book: array, position: int, total: int, prev: ?string, next: ?string}|null
|
||||
*/
|
||||
public function findForArticle(string $articleSlug): ?array
|
||||
{
|
||||
foreach ($this->getAll() as $book) {
|
||||
$arts = $book['articles'] ?? [];
|
||||
$pos = array_search($articleSlug, $arts, true);
|
||||
if ($pos === false) {
|
||||
continue;
|
||||
}
|
||||
$pos = (int) $pos;
|
||||
return [
|
||||
'book' => $book,
|
||||
'position' => $pos + 1,
|
||||
'total' => count($arts),
|
||||
'prev' => $pos > 0 ? $arts[$pos - 1] : null,
|
||||
'next' => $pos < count($arts) - 1 ? $arts[$pos + 1] : null,
|
||||
];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Écriture
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
public function save(array $book): void
|
||||
{
|
||||
$slug = $this->sanitizeSlug($book['slug'] ?? '');
|
||||
if ($slug === '') {
|
||||
return;
|
||||
}
|
||||
$book['slug'] = $slug;
|
||||
$book['articles'] = array_values(array_filter(array_map('strval', $book['articles'] ?? [])));
|
||||
if (!is_dir($this->booksDir)) {
|
||||
mkdir($this->booksDir, 0755, true);
|
||||
}
|
||||
file_put_contents(
|
||||
$this->bookPath($slug),
|
||||
json_encode($book, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n"
|
||||
);
|
||||
}
|
||||
|
||||
public function delete(string $slug): void
|
||||
{
|
||||
$path = $this->bookPath($slug);
|
||||
if (file_exists($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
private function bookPath(string $slug): string
|
||||
{
|
||||
return $this->booksDir . '/' . $slug . '.json';
|
||||
}
|
||||
|
||||
private function sanitizeSlug(string $slug): string
|
||||
{
|
||||
$map = [
|
||||
'à' => 'a', 'â' => 'a', 'ä' => 'a',
|
||||
'é' => 'e', 'è' => 'e', 'ê' => 'e', 'ë' => 'e',
|
||||
'î' => 'i', 'ï' => 'i',
|
||||
'ô' => 'o', 'ö' => 'o',
|
||||
'ù' => 'u', 'û' => 'u', 'ü' => 'u',
|
||||
'ç' => 'c', 'æ' => 'ae', 'œ' => 'oe',
|
||||
];
|
||||
$slug = mb_strtolower(strtr(trim($slug), $map), 'UTF-8');
|
||||
$slug = (string) preg_replace('/[^a-z0-9]+/', '-', $slug);
|
||||
return trim($slug, '-');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user