1. Architecture complète du projet
Structure des dossiers
blog-mvc/ │
├── public/
│ ├── index.php
│ ├── css/
│ │ └── style.css
│ └── .htaccess
│ ├── 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
│ └── Database.php
│ ├── config/
│ └──
config.php
│ ├── autoload.php
├── composer.json
└── database.sql
2. Configuration
config/config.php
<?php
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__));
if (session_status() ===
PHP_SESSION_NONE) { session_start(); }
?>
database.sql - Structure de la BDD
CREATE DATABASE IF NOT EXISTS blog_mvc CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE
blog_mvc;
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) );
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 );
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 !