From 03ce72ce0082574bd98c5604dde8b47cd6d3c997 Mon Sep 17 00:00:00 2001 From: julien Date: Mon, 9 Mar 2026 16:07:17 +0100 Subject: [PATCH] Varous improvements --- README.md | 8 +- public/index.php | 62 +++++++--------- src/Config.php | 34 +++++++++ src/Controllers/PostController.php | 111 ++++++++++++---------------- src/Repositories/PostRepository.php | 66 +++++++++++++++++ src/Routes.php | 22 ++++++ src/Services/HtmlSanitizer.php | 8 +- 7 files changed, 203 insertions(+), 108 deletions(-) create mode 100644 src/Config.php create mode 100644 src/Repositories/PostRepository.php create mode 100644 src/Routes.php diff --git a/README.md b/README.md index ce808e2..bd01cde 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ * [PHP](https://www.php.net) comme langage * [Slim](https://www.slimframework.com) comme framework -* [RedBeanPHP](https://www.redbeanphp.com/index.php) comme ORM +* [Medoo](https://medoo.in) comme ORM * [Twig](https://twig.symfony.com) comme template engine ## HOWTO @@ -12,12 +12,6 @@ Installer les dépendances : $ composer install ``` -Initialiser la base de données : -``` -$ mkdir database -$ php scripts/migrations.php -``` - Lancer le serveur de développement : ``` $ php -S localhost:8080 -t public diff --git a/public/index.php b/public/index.php index 7d01da0..b457fe2 100644 --- a/public/index.php +++ b/public/index.php @@ -5,16 +5,22 @@ declare(strict_types=1); require __DIR__ . '/../vendor/autoload.php'; use Dotenv\Dotenv; - -// Charger les variables d'environnement -$dotenv = Dotenv::createImmutable(__DIR__ . '/..'); -$dotenv->load(); - use Slim\Factory\AppFactory; use Slim\Views\TwigMiddleware; use Slim\Views\Twig; use Medoo\Medoo; use App\Controllers\PostController; +use App\Repositories\PostRepository; +use App\Services\HtmlSanitizer; +use App\Routes; +use App\Config; + +// ============================================ +// Charger les variables d'environnement +// ============================================ + +$dotenv = Dotenv::createImmutable(__DIR__ . '/..'); +$dotenv->load(); // ============================================ // Configuration @@ -23,26 +29,6 @@ use App\Controllers\PostController; $env = $_ENV['APP_ENV'] ?? 'production'; $isDev = strtolower($env) === 'development'; -// Dossier de cache Twig (false en dev, chemin en prod) -$twigCache = false; -if (!$isDev) { - $twigCache = __DIR__ . '/../var/cache/twig'; - if (!is_dir($twigCache)) { - @mkdir($twigCache, 0755, true); - } -} - -// Chemin base de données -$dbFile = __DIR__ . '/../database/app.sqlite'; -$dbDir = dirname($dbFile); -if (!is_dir($dbDir)) { - @mkdir($dbDir, 0755, true); -} -if (!file_exists($dbFile)) { - @touch($dbFile); - @chmod($dbFile, 0664); -} - // ============================================ // Initialisation des services // ============================================ @@ -50,10 +36,11 @@ if (!file_exists($dbFile)) { // Twig $twig = Twig::create( __DIR__ . '/../views', - ['cache' => $twigCache] + ['cache' => Config::getTwigCache($isDev)] ); // Medoo (SQLite) +$dbFile = Config::getDatabasePath(); $db = new Medoo([ 'type' => 'sqlite', 'database' => $dbFile, @@ -71,8 +58,11 @@ $db->pdo->exec(" ) "); -// Sanitizer HTML -$htmlSanitizer = new \App\Services\HtmlSanitizer(); +// HtmlSanitizer +$htmlSanitizer = new HtmlSanitizer(); + +// PostRepository +$postRepository = new PostRepository($db); // ============================================ // Slim App @@ -86,19 +76,17 @@ $app->add(TwigMiddleware::create($app, $twig)); // Routes // ============================================ -$controller = new PostController($twig, $db, $htmlSanitizer); +$controller = new PostController($twig, $postRepository, $htmlSanitizer); +Routes::register($app, $controller); -$app->get('/', [$controller, 'index']); -$app->get('/article/{slug}', [$controller, 'show']); -$app->get('/admin', [$controller, 'admin']); -$app->get('/admin/edit/{id}', [$controller, 'form']); -$app->post('/admin/create', [$controller, 'create']); -$app->post('/admin/edit/{id}', [$controller, 'update']); -$app->post('/admin/delete/{id}', [$controller, 'delete']); +// ============================================ +// Error Handling +// ============================================ + +$errorMiddleware = $app->addErrorMiddleware($isDev, $isDev, $isDev); // ============================================ // Run // ============================================ -$errorMiddleware = $app->addErrorMiddleware($isDev, $isDev, $isDev); $app->run(); diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 0000000..111c5bf --- /dev/null +++ b/src/Config.php @@ -0,0 +1,34 @@ +db->select('post', '*', ['ORDER' => ['id' => 'DESC']]); - $posts = array_map(fn ($row) => Post::fromArray($row), $rows ?: []); - + $posts = $this->postRepository->findAll(); return $this->view->render($res, 'pages/home.twig', ['posts' => $posts]); } @@ -39,16 +38,15 @@ class PostController public function show(Request $req, Response $res, array $args): Response { $slug = (string)($args['slug'] ?? ''); - $row = $this->db->get('post', '*', ['slug' => $slug]); + $post = $this->postRepository->findBySlug($slug); - if (!$row) { + if (!$post) { $res->getBody()->write('Article non trouvé'); return $res->withStatus(404); } - $post = Post::fromArray($row); - // Nettoyer le contenu avant de l'envoyer au template - $post = $post->withSanitizedContent($this->sanitizer->sanitize($post->getContent())); + $sanitizedContent = $this->sanitizer->sanitize($post->getContent()); + $post = $post->withSanitizedContent($sanitizedContent); return $this->view->render($res, 'pages/post_detail.twig', ['post' => $post]); } @@ -58,9 +56,7 @@ class PostController */ public function admin(Request $req, Response $res): Response { - $rows = $this->db->select('post', '*', ['ORDER' => ['id' => 'DESC']]); - $posts = array_map(fn ($row) => Post::fromArray($row), $rows ?: []); - + $posts = $this->postRepository->findAll(); return $this->view->render($res, 'pages/admin.twig', ['posts' => $posts]); } @@ -73,12 +69,11 @@ class PostController $post = null; if ($id > 0) { - $row = $this->db->get('post', '*', ['id' => $id]); - if (!$row) { + $post = $this->postRepository->findById($id); + if (!$post) { $res->getBody()->write('Article non trouvé'); return $res->withStatus(404); } - $post = Post::fromArray($row); } $action = $id ? "/admin/edit/{$id}" : "/admin/create"; @@ -97,41 +92,19 @@ class PostController $title = trim((string)($data['title'] ?? '')); $content = trim((string)($data['content'] ?? '')); - $post = new Post(0, $title, $content); - $slug = $post->getSlug(); - - // Gérer les collisions - $slug = $this->ensureUniqueSlug($slug); - - $this->db->insert('post', [ - 'title' => $post->getTitle(), - 'content' => $post->getContent(), - 'slug' => $slug, - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]); - - return $res->withHeader('Location', '/admin')->withStatus(302); - } - - private function ensureUniqueSlug(string $slug, ?int $excludeId = null): string - { - $original = $slug; - $counter = 1; - - while (true) { - $existing = $this->db->get('post', 'id', ['slug' => $slug]); - - // Si pas de collision, ou si c'est le même post, le slug est unique - if (!$existing || ($excludeId && $existing === $excludeId)) { - break; - } - - $slug = $original . '-' . $counter; - $counter++; + try { + $post = new Post(0, $title, $content); + } catch (\InvalidArgumentException) { + // Validation échouée, retour à l'admin + return $res->withHeader('Location', '/admin')->withStatus(302); } - return $slug; + $slug = $post->getSlug(); + $slug = $this->ensureUniqueSlug($slug); + + $this->postRepository->create($post, $slug); + + return $res->withHeader('Location', '/admin')->withStatus(302); } /** @@ -140,7 +113,7 @@ class PostController public function update(Request $req, Response $res, array $args): Response { $id = (int)$args['id']; - $existing = $this->db->get('post', 'id', ['id' => $id]); + $existing = $this->postRepository->findById($id); if (!$existing) { $res->getBody()->write('Article non trouvé'); @@ -151,7 +124,6 @@ class PostController $title = trim((string)($data['title'] ?? '')); $content = trim((string)($data['content'] ?? '')); - // Créer un objet Post pour valider try { $post = new Post($id, $title, $content); } catch (\InvalidArgumentException) { @@ -159,17 +131,10 @@ class PostController return $res->withHeader('Location', '/admin')->withStatus(302); } - // Calculer le nouveau slug $newSlug = $post->getSlug(); - $newSlug = $this->ensureUniqueSlug($newSlug, $id); // Passer l'ID pour exclure le post actuel + $newSlug = $this->ensureUniqueSlug($newSlug, $id); - // Persister en DB - $this->db->update('post', [ - 'title' => $post->getTitle(), - 'content' => $post->getContent(), - 'slug' => $newSlug, - 'updated_at' => date('Y-m-d H:i:s'), - ], ['id' => $id]); + $this->postRepository->update($id, $post, $newSlug); return $res->withHeader('Location', '/admin')->withStatus(302); } @@ -180,14 +145,34 @@ class PostController public function delete(Request $req, Response $res, array $args): Response { $id = (int)$args['id']; - $existing = $this->db->get('post', 'id', ['id' => $id]); + $existing = $this->postRepository->findById($id); if (!$existing) { $res->getBody()->write('Article non trouvé'); return $res->withStatus(404); } - $this->db->delete('post', ['id' => $id]); + $this->postRepository->delete($id); return $res->withHeader('Location', '/admin')->withStatus(302); } + + /** + * Assure l'unicité d'un slug en ajoutant un suffixe numérique si nécessaire. + * + * @param string $slug Le slug de base + * @param int|null $excludeId L'ID du post à exclure de la vérification (pour les mises à jour) + * @return string Le slug unique + */ + private function ensureUniqueSlug(string $slug, ?int $excludeId = null): string + { + $original = $slug; + $counter = 1; + + while ($this->postRepository->slugExists($slug, $excludeId)) { + $slug = $original . '-' . $counter; + $counter++; + } + + return $slug; + } } diff --git a/src/Repositories/PostRepository.php b/src/Repositories/PostRepository.php new file mode 100644 index 0000000..be95673 --- /dev/null +++ b/src/Repositories/PostRepository.php @@ -0,0 +1,66 @@ +db->select('post', '*', ['ORDER' => ['id' => 'DESC']]); + return array_map(fn ($row) => Post::fromArray($row), $rows ?: []); + } + + public function findBySlug(string $slug): ?Post + { + $row = $this->db->get('post', '*', ['slug' => $slug]); + return $row ? Post::fromArray($row) : null; + } + + public function findById(int $id): ?Post + { + $row = $this->db->get('post', '*', ['id' => $id]); + return $row ? Post::fromArray($row) : null; + } + + public function create(Post $post, string $slug): int + { + $this->db->insert('post', [ + 'title' => $post->getTitle(), + 'content' => $post->getContent(), + 'slug' => $slug, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + return (int) $this->db->id(); + } + + public function update(int $id, Post $post, string $slug): void + { + $this->db->update('post', [ + 'title' => $post->getTitle(), + 'content' => $post->getContent(), + 'slug' => $slug, + 'updated_at' => date('Y-m-d H:i:s'), + ], ['id' => $id]); + } + + public function delete(int $id): void + { + $this->db->delete('post', ['id' => $id]); + } + + public function slugExists(string $slug, ?int $excludeId = null): bool + { + $existing = $this->db->get('post', 'id', ['slug' => $slug]); + return $existing && (!$excludeId || $existing !== $excludeId); + } +} diff --git a/src/Routes.php b/src/Routes.php new file mode 100644 index 0000000..127a37c --- /dev/null +++ b/src/Routes.php @@ -0,0 +1,22 @@ +get('/', [$controller, 'index']); + $app->get('/article/{slug}', [$controller, 'show']); + $app->get('/admin', [$controller, 'admin']); + $app->get('/admin/edit/{id}', [$controller, 'form']); + $app->post('/admin/create', [$controller, 'create']); + $app->post('/admin/edit/{id}', [$controller, 'update']); + $app->post('/admin/delete/{id}', [$controller, 'delete']); + } +} diff --git a/src/Services/HtmlSanitizer.php b/src/Services/HtmlSanitizer.php index 5792c73..faa8ae6 100644 --- a/src/Services/HtmlSanitizer.php +++ b/src/Services/HtmlSanitizer.php @@ -13,6 +13,12 @@ final class HtmlSanitizer public function __construct() { + // Créer le répertoire de cache s'il n'existe pas + $cacheDir = __DIR__ . '/../../var/cache/htmlpurifier'; + if (!is_dir($cacheDir)) { + @mkdir($cacheDir, 0755, true); + } + $config = HTMLPurifier_Config::createDefault(); // Autoriser les balises courantes de formatage $config->set('HTML.Allowed', 'p,br,strong,em,u,h1,h2,h3,h4,h5,h6,ul,ol,li,blockquote,a[href],img[src|alt|width|height]'); @@ -20,7 +26,7 @@ final class HtmlSanitizer $config->set('HTML.AllowedAttributes', 'href,src,alt,width,height,title'); // Activer le cache $config->set('Cache.DefinitionImpl', 'Serializer'); - $config->set('Cache.SerializerPath', __DIR__ . '/../../var/cache/htmlpurifier'); + $config->set('Cache.SerializerPath', $cacheDir); $this->purifier = new HTMLPurifier($config); }