Semaine 4 - Jour 5 : Projet MVC Complet (Blog)

Aujourd'hui, nous construisons un projet MVC complet : un blog avec authentification, CRUD d'articles, et toutes les bonnes pratiques professionnelles.

1. Architecture complète du projet

Structure des dossiers

blog-mvc/ │
├── public/ // Seul dossier accessible publiquement
│ ├── index.php // Point d'entrée unique
│ ├── css/
│ │ └── style.css
│ └── .htaccess // Réécriture d'URL (Apache)
│ ├── src/
│ ├── Controllers/
│ │ ├── HomeController.php
│ │ ├── ArticleController.php
│ │ └── AuthController.php
│ │ │ ├── Models/
│ │ ├── Article.php
│ │ └── User.php
│ │ │ ├── Views/
│ │ ├── layout/
│ │ │ ├── header.php
│ │ │ └── footer.php
│ │ ├── home/
│ │ │ └── index.php
│ │ ├── articles/
│ │ │ ├── index.php
│ │ │ ├── show.php
│ │ │ ├── create.php
│ │ │ └── edit.php │ │ └── auth/
│ │ ├── login.php
│ │ └── register.php
│ │ │ └── Core/
│ ├── Router.php // Routeur amélioré
│ └── Database.php // Connexion PDO
│ ├── config/
│ └── config.php // Configuration globale
│ ├── autoload.php // Autoloader PSR-4
├── composer.json // Dépendances Composer
└── database.sql // Structure de la BDD

2. Configuration

config/config.php

<?php // config/config.php - Configuration globale define('DB_HOST', 'localhost'); define('DB_NAME', 'blog_mvc'); define('DB_USER', 'root'); define('DB_PASS', ''); define('BASE_URL', 'http://localhost/blog-mvc/public'); define('ROOT_PATH', dirname(__DIR__)); // Démarrer la session if (session_status() === PHP_SESSION_NONE) { session_start(); } ?>

database.sql - Structure de la BDD

-- Création de la base de données CREATE DATABASE IF NOT EXISTS blog_mvc CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE blog_mvc; -- Table users CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, nom VARCHAR(100) NOT NULL, prenom VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, role ENUM('user', 'admin') DEFAULT 'user', date_inscription DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_email (email) ); -- Table articles CREATE TABLE articles ( id INT AUTO_INCREMENT PRIMARY KEY, titre VARCHAR(200) NOT NULL, contenu TEXT NOT NULL, user_id INT NOT NULL, date_creation DATETIME DEFAULT CURRENT_TIMESTAMP, date_modification DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Données de test INSERT INTO users (nom, prenom, email, password, role) VALUES ('Admin', 'Super', 'admin@blog.com', '$2y$10$hash...', 'admin'), ('Dupont', 'Jean', 'jean@example.com', '$2y$10$hash...', 'user');

3. Core - Database (Singleton)

src/Core/Database.php

<?php namespace Core; use PDO; use PDOException; class Database { private static $instance = null; private $pdo; private function __construct() { try { $this->pdo = new PDO( "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4", DB_USER, DB_PASS, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false ] ); } catch (PDOException $e) { error_log($e->getMessage()); die("Erreur de connexion à la base de données"); } } public static function getInstance() { if (self::$instance === null) { self::$instance = new self(); } return self::$instance->pdo; } } ?>

4. Modèle Article

src/Models/Article.php

<?php namespace Models; use Core\Database; class Article { private $pdo; public function __construct() { $this->pdo = Database::getInstance(); } public function getAll() { $sql = "SELECT a.*, u.prenom, u.nom FROM articles a JOIN users u ON a.user_id = u.id ORDER BY a.date_creation DESC"; return $this->pdo->query($sql)->fetchAll(); } public function getById($id) { $sql = "SELECT a.*, u.prenom, u.nom FROM articles a JOIN users u ON a.user_id = u.id WHERE a.id = :id"; $stmt = $this->pdo->prepare($sql); $stmt->execute(['id' => $id]); return $stmt->fetch(); } public function create($titre, $contenu, $userId) { $sql = "INSERT INTO articles (titre, contenu, user_id) VALUES (:titre, :contenu, :user_id)"; $stmt = $this->pdo->prepare($sql); $stmt->execute([ 'titre' => $titre, 'contenu' => $contenu, 'user_id' => $userId ]); return $this->pdo->lastInsertId(); } public function update($id, $titre, $contenu) { $sql = "UPDATE articles SET titre = :titre, contenu = :contenu WHERE id = :id"; $stmt = $this->pdo->prepare($sql); return $stmt->execute(['id' => $id, 'titre' => $titre, 'contenu' => $contenu]); } public function delete($id) { $sql = "DELETE FROM articles WHERE id = :id"; $stmt = $this->pdo->prepare($sql); return $stmt->execute(['id' => $id]); } } ?>

5. Modèle User (Authentification)

src/Models/User.php

<?php namespace Models; use Core\Database; class User { private $pdo; public function __construct() { $this->pdo = Database::getInstance(); } public function register($nom, $prenom, $email, $password) { $hashedPassword = password_hash($password, PASSWORD_DEFAULT); $sql = "INSERT INTO users (nom, prenom, email, password) VALUES (:nom, :prenom, :email, :password)"; $stmt = $this->pdo->prepare($sql); return $stmt->execute([ 'nom' => $nom, 'prenom' => $prenom, 'email' => $email, 'password' => $hashedPassword ]); } public function login($email, $password) { $sql = "SELECT * FROM users WHERE email = :email"; $stmt = $this->pdo->prepare($sql); $stmt->execute(['email' => $email]); $user = $stmt->fetch(); if ($user && password_verify($password, $user['password'])) { return $user; } return false; } public function findByEmail($email) { $sql = "SELECT * FROM users WHERE email = :email"; $stmt = $this->pdo->prepare($sql); $stmt->execute(['email' => $email]); return $stmt->fetch(); } } ?>

6. Contrôleurs

src/Controllers/ArticleController.php

<?php namespace Controllers; use Models\Article; class ArticleController { private $articleModel; public function __construct() { $this->articleModel = new Article(); } public function index() { $articles = $this->articleModel->getAll(); require ROOT_PATH . '/src/Views/articles/index.php'; } public function show($id) { $article = $this->articleModel->getById($id); if (!$article) { die("Article non trouvé"); } require ROOT_PATH . '/src/Views/articles/show.php'; } public function create() { if (!isset($_SESSION['user_id'])) { header('Location: ' . BASE_URL . '/index.php?action=login'); exit(); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $titre = trim($_POST['titre'] ?? ''); $contenu = trim($_POST['contenu'] ?? ''); if (!empty($titre) && !empty($contenu)) { $id = $this->articleModel->create($titre, $contenu, $_SESSION['user_id']); header('Location: ' . BASE_URL . '/index.php?action=show&id=' . $id); exit(); } } require ROOT_PATH . '/src/Views/articles/create.php'; } } ?>

src/Controllers/AuthController.php

<?php namespace Controllers; use Models\User; class AuthController { private $userModel; public function __construct() { $this->userModel = new User(); } public function login() { if ($_SERVER['REQUEST_METHOD'] === 'POST') { $email = trim($_POST['email'] ?? ''); $password = $_POST['password'] ?? ''; $user = $this->userModel->login($email, $password); if ($user) { session_regenerate_id(true); $_SESSION['user_id'] = $user['id']; $_SESSION['user_nom'] = $user['prenom'] . ' ' . $user['nom']; header('Location: ' . BASE_URL . '/index.php'); exit(); } } require ROOT_PATH . '/src/Views/auth/login.php'; } public function logout() { session_destroy(); header('Location: ' . BASE_URL . '/index.php'); exit(); } } ?>

7. Point d'entrée

public/index.php

<?php require_once __DIR__ . '/../config/config.php'; require_once __DIR__ . '/../autoload.php'; use Controllers\HomeController; use Controllers\ArticleController; use Controllers\AuthController; $action = $_GET['action'] ?? 'home'; $id = $_GET['id'] ?? null; try { switch ($action) { case 'home': $controller = new HomeController(); $controller->index(); break; case 'articles': $controller = new ArticleController(); $controller->index(); break; case 'show': if ($id) { $controller = new ArticleController(); $controller->show($id); } break; case 'create': $controller = new ArticleController(); $controller->create(); break; case 'login': $controller = new AuthController(); $controller->login(); break; case 'logout': $controller = new AuthController(); $controller->logout(); break; default: die("Action inconnue"); } } catch (\Exception $e) { error_log($e->getMessage()); die("Une erreur s'est produite"); } ?>

8. Bonnes pratiques

Sécurité

  • PDO avec requêtes préparées
  • password_hash() / password_verify()
  • htmlspecialchars() partout
  • session_regenerate_id()
  • Validation côté serveur

Architecture

  • Séparation MVC stricte
  • Namespaces PSR-4
  • Autoloading automatique
  • Singleton pour Database
  • Point d'entrée unique

Checklist de sécurité finale

  • Toutes les requêtes SQL sont préparées
  • Tous les affichages utilisent htmlspecialchars()
  • Les mots de passe sont hachés avec password_hash()
  • Les sessions sont régénérées après connexion
  • Les pages protégées vérifient $_SESSION['user_id']
  • Les erreurs sont loggées, pas affichées
  • Validation côté serveur obligatoire
Froggiesplaining :


Objectifs de ce cours :
✅ Construire un projet MVC complet de A à Z
✅ Implémenter un blog avec CRUD d'articles
✅ Ajouter un système d'authentification sécurisé
✅ Appliquer toutes les bonnes pratiques professionnelles
✅ Utiliser Database Singleton et pattern MVC strict
✅ Sécuriser l'application (PDO, password_hash, htmlspecialchars)

Points clés à retenir :
config/ = Les règles de la mare (DB_HOST, DB_NAME, sessions)
src/Models/ = Réserve de données (Article, User avec méthodes CRUD)
src/Controllers/ = Orchestrateurs (ArticleController, AuthController)
src/Views/ = Miroirs d'affichage (articles/index.php, auth/login.php)
public/ = Point d'entrée unique accessible (index.php avec router)
Singleton Database = Une seule instance PDO partagée partout
Sécurité = PDO préparé + password_hash() + htmlspecialchars() + sessions
Authentification = register(), login(), logout(), session_regenerate_id()

Exercice pratique :
1. Créer la structure complète du blog MVC (config, src, public)
2. Créer la base de données avec tables users et articles
3. Implémenter Core\Database avec pattern Singleton
4. Créer Models\User avec register(), login(), findByEmail()
5. Créer Models\Article avec getAll(), getById(), create(), update(), delete()
6. Créer Controllers\AuthController pour gérer connexion/inscription
7. Créer Controllers\ArticleController pour gérer les articles
8. Créer toutes les vues nécessaires (login, liste articles, détail article)
9. Implémenter le router dans public/index.php avec switch sur action
10. Tester tout le flux: inscription → connexion → créer article → voir article
11. Conseil final de Froggie: Tu as maintenant toutes les bases pour créer des applications PHP professionnelles ! Continue à pratiquer, explore les frameworks (Laravel, Symfony), et surtout: CODE, CODE, CODE ! La pratique rend parfait. Bravo pour avoir complété cette formation PHP !

Froggie explain

GitHub - eCrea