Intermédiaire120 min80 XP
Objectifs de cette leçon
- Construire un blog CRUD complet
- Gérer le routage et les vues
- Sécuriser l'application
Projet Final PHP : Blog Complet 🏆
Mets en pratique toutes tes connaissances PHP en construisant un mini-blog avec authentification, articles et gestion d'erreurs.
Architecture du projet
blog/ ├── config/ │ ├── database.php // Connexion PDO │ └── config.php // Paramètres globaux ├── public/ │ ├── index.php // Page d'accueil │ ├── login.php // Connexion │ ├── register.php // Inscription │ ├── article.php // Voir un article │ ├── new-article.php // Créer un article │ └── logout.php // Déconnexion ├── src/ │ ├── models/ │ │ ├── User.php │ │ └── Article.php │ └── helpers.php └── sql/ └── schema.sql
Configuration et base de données
<?php // config/database.php function getDb(): PDO { static $pdo = null; if ($pdo === null) { $pdo = new PDO( "mysql:host=localhost;dbname=blog;charset=utf8mb4", "root", "", [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false ] ); } return $pdo; } ?>
-- sql/schema.sql CREATE DATABASE IF NOT EXISTS blog; USE blog; CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, nom VARCHAR(100) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, mdp_hash VARCHAR(255) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE articles ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, titre VARCHAR(255) NOT NULL, contenu TEXT NOT NULL, slug VARCHAR(255) UNIQUE NOT NULL, published BOOLEAN DEFAULT TRUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE );
Le modèle User
<?php // src/models/User.php class User { private PDO $db; public function __construct() { $this->db = getDb(); } public function register(string $nom, string $email, string $mdp): int { if ($this->findByEmail($email)) { throw new Exception("Cet email est déjà utilisé"); } $stmt = $this->db->prepare(" INSERT INTO users (nom, email, mdp_hash) VALUES (?, ?, ?) "); $stmt->execute([$nom, $email, password_hash($mdp, PASSWORD_DEFAULT)]); return (int)$this->db->lastInsertId(); } public function login(string $email, string $mdp): ?array { $user = $this->findByEmail($email); if ($user && password_verify($mdp, $user['mdp_hash'])) { return $user; } return null; } public function findByEmail(string $email): ?array { $stmt = $this->db->prepare("SELECT * FROM users WHERE email = ?"); $stmt->execute([$email]); return $stmt->fetch() ?: null; } public function findById(int $id): ?array { $stmt = $this->db->prepare("SELECT id, nom, email, created_at FROM users WHERE id = ?"); $stmt->execute([$id]); return $stmt->fetch() ?: null; } } ?>
Le modèle Article
<?php // src/models/Article.php class Article { private PDO $db; public function __construct() { $this->db = getDb(); } public function create(int $userId, string $titre, string $contenu): int { $slug = $this->genererSlug($titre); $stmt = $this->db->prepare(" INSERT INTO articles (user_id, titre, contenu, slug) VALUES (?, ?, ?, ?) "); $stmt->execute([$userId, $titre, $contenu, $slug]); return (int)$this->db->lastInsertId(); } public function findById(int $id): ?array { $stmt = $this->db->prepare(" SELECT a.*, u.nom as auteur FROM articles a JOIN users u ON a.user_id = u.id WHERE a.id = ? "); $stmt->execute([$id]); return $stmt->fetch() ?: null; } public function findAll(int $page = 1, int $parPage = 10): array { $offset = ($page - 1) * $parPage; $stmt = $this->db->prepare(" SELECT a.*, u.nom as auteur FROM articles a JOIN users u ON a.user_id = u.id WHERE a.published = TRUE ORDER BY a.created_at DESC LIMIT ? OFFSET ? "); $stmt->execute([$parPage, $offset]); return $stmt->fetchAll(); } public function count(): int { return (int)$this->db->query( "SELECT COUNT(*) FROM articles WHERE published = TRUE" )->fetchColumn(); } public function delete(int $id, int $userId): bool { $stmt = $this->db->prepare(" DELETE FROM articles WHERE id = ? AND user_id = ? "); return $stmt->execute([$id, $userId]); } private function genererSlug(string $titre): string { $slug = strtolower(trim($titre)); $slug = preg_replace('/[^a-z0-9-]/', '-', $slug); $slug = preg_replace('/-+/', '-', $slug); return rtrim($slug, '-'); } } ?>
Pages du blog
<?php // public/index.php — Page d'accueil session_start(); require_once __DIR__ . '/../config/database.php'; require_once __DIR__ . '/../src/helpers.php'; require_once __DIR__ . '/../src/models/User.php'; require_once __DIR__ . '/../src/models/Article.php'; $articleModel = new Article(); $page = max(1, (int)($_GET['page'] ?? 1)); $articles = $articleModel->findAll($page); $total = $articleModel->count(); $totalPages = ceil($total / 10); render('header', ['titre' => 'Blog PHP']); ?> <div class="articles"> <?php if (empty($articles)): ?> <p>Aucun article pour le moment.</p> <?php else: ?> <?php foreach ($articles as $a): ?> <article> <h2> <a href="article.php?id=<?= $a['id'] ?>"> <?= e($a['titre']) ?> </a> </h2> <p class="meta"> Par <?= e($a['auteur']) ?> le <?= date('d/m/Y', strtotime($a['created_at'])) ?> </p> <p><?= e(substr($a['contenu'], 0, 200)) ?>...</p> </article> <?php endforeach; ?> <?php endif; ?> </div> <?php // Pagination if ($totalPages > 1): ?> <nav class="pagination"> <?php for ($i = 1; $i <= $totalPages; $i++): ?> <a href="?page=<?= $i ?>" class="<?= $i === $page ? 'active' : '' ?>"> <?= $i ?> </a> <?php endfor; ?> </nav> <?php endif; render('footer'); ?>
Système de templates
<?php // src/helpers.php function e(string $texte): string { return htmlspecialchars($texte, ENT_QUOTES, 'UTF-8'); } function render(string $template, array $data = []): void { extract($data); $fichier = __DIR__ . '/../templates/' . $template . '.php'; if (file_exists($fichier)) { require $fichier; } } function setFlash(string $type, string $message): void { $_SESSION['flash'] = ['type' => $type, 'message' => $message]; } function getFlash(): string { if (isset($_SESSION['flash'])) { $type = $_SESSION['flash']['type']; $message = $_SESSION['flash']['message']; unset($_SESSION['flash']); return '<div class="alert alert-' . $type . '">' . $message . '</div>'; } return ''; } function requireLogin(): void { session_start(); if (!isset($_SESSION['user_id'])) { header('Location: login.php'); exit; } } ?>
Connexion sécurisée
<?php // public/login.php session_start(); require_once __DIR__ . '/../config/database.php'; require_once __DIR__ . '/../src/helpers.php'; require_once __DIR__ . '/../src/models/User.php'; $erreurs = []; if ($_SERVER["REQUEST_METHOD"] === "POST") { $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL); $mdp = $_POST['mdp'] ?? ''; $user = new User(); $utilisateur = $user->login($email, $mdp); if ($utilisateur) { session_regenerate_id(true); $_SESSION['user_id'] = $utilisateur['id']; $_SESSION['user_nom'] = $utilisateur['nom']; header("Location: index.php"); exit; } else { $erreurs[] = "Email ou mot de passe incorrect"; } } ?>
Checklist de fonctionnalités
| Fonctionnalité | Statut |
|---|---|
| Inscription avec validation | ✅ |
| Connexion sécurisée (password_hash) | ✅ |
| CRUD d'articles | ✅ |
| Slugs pour les URLs | ✅ |
| Pagination | ✅ |
| Protection XSS (htmlspecialchars) | ✅ |
| Requêtes préparées | ✅ |
| Sessions sécurisées | ✅ |
| Template system | ✅ |
| Messages flash | ✅ |
Extensions possibles
- Système de commentaires
- Catégories et tags
- Recherche full-text
- Upload d'images
- API REST
- Dashboard admin
🎯 Félicitations ! Tu as construit une application PHP complète. Ce projet couvre les fondamentaux : POO, BDD, sessions, formulaires, et sécurité. Continue à l'enrichir et passe à un framework (Laravel, Symfony) pour les projets plus complexes.
Prochaine section : Node.js — le JavaScript côté serveur !
⚡ Simulateur — Projet : Blog CRUD
Étape 1/11📝 Pseudo-code
1<?php
2$pdo = new PDO("mysql:host=localhost;dbname=blog", "root", "");
3$action = $_GET["action"] ?? "list";
4
5if ($action === "list") {
6 $stmt = $pdo->query("SELECT * FROM posts ORDER BY id DESC");
7 while ($post = $stmt->fetch()) {
8 echo $post["titre"];
9 echo $post["contenu"];
10 }
11} elseif ($action === "create" && $_SERVER["REQUEST_METHOD"] === "POST") {
12 $titre = $_POST["titre"];
13 $contenu = $_POST["contenu"];
14 $stmt = $pdo->prepare("INSERT INTO posts (titre, contenu) VALUES (?, ?)");
15 $stmt->execute([$titre, $contenu]);
16 echo "Post créé !";
17}
18?>
⚙️ Exécution
📦 Mémoire
Aucune variable
🔀 Flux d'exécution
<?php
3
Début du script
Ouverture du bloc PHP du controller principal du blog.