0 XP
?
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.