Tous les articles

Construire un CMS maison en PHP

PHPArchitectureCMS
Construire un CMS maison en PHP

Plutôt que d'installer WordPress ou Drupal pour un simple site CV, j'ai choisi de construire mon propre CMS en PHP 8 : un petit framework MVC maison et un back-office sur mesure. Ce site — celui que vous lisez — en est le résultat. Voici comment il est conçu.

Schéma du cycle d'une requête dans l'architecture MVC : navigateur, front controller, routeur, contrôleur, modèle, MySQL, vue Twig.
Le cycle d'une requête : un point d'entrée unique, puis chaque couche a une seule responsabilité.

Pourquoi un CMS maison ?

Un CMS « tout fait » apporte des centaines de fonctionnalités… dont on n'utilise que 5 %. En partant de zéro, on obtient un code léger, compris de bout en bout et sans dette technique inutile. C'est aussi un excellent terrain pour maîtriser des fondamentaux : routage, requêtes préparées, sessions, sécurité. L'objectif n'est pas de réinventer Symfony, mais d'avoir juste ce qu'il faut, parfaitement adapté au besoin.

Une architecture en couches

Le projet suit le patron MVC (Modèle — Vue — Contrôleur), organisé en trois familles : un cœur réutilisable, la logique applicative, et la présentation.

Organisation du code en trois couches : cœur (src/Core), application (contrôleurs et modèles), présentation (templates Twig et assets).
Chaque dossier a un rôle clair, ce qui rend le code prévisible et facile à faire évoluer.

Un point d'entrée unique

Toutes les requêtes passent par public/index.php (le front controller). Un fichier .htaccess réécrit les URLs vers ce fichier, qui démarre l'application : session, routeur, gestion des erreurs.

// public/index.php
require dirname(__DIR__) . '/vendor/autoload.php';
(new App\Core\App())->run();

Le routeur

Les routes sont déclarées dans un tableau et associent méthode HTTP + chemin à un couple [Contrôleur, action]. Les segments dynamiques (comme {slug}) sont transformés en expressions régulières.

['GET',  '/blog/{slug}', [BlogController::class, 'show']],
['POST', '/contact',     [ContactController::class, 'send']],

Au moment de la requête, le routeur cherche la première route qui correspond, instancie le contrôleur et appelle l'action avec les paramètres extraits de l'URL.

La couche base de données

L'accès aux données repose sur PDO, avec une règle non négociable : toute valeur passe par une requête préparée. Aucune donnée utilisateur n'est concaténée dans une chaîne SQL — c'est la première barrière contre les injections.

public function all(string $sql, array $params = []): array
{
    $stmt = $this->pdo->prepare($sql);
    $stmt->execute($params);
    return $stmt->fetchAll();
}

Par-dessus, un petit modèle de base fournit le CRUD générique (créer, lire, mettre à jour, supprimer) et chaque entité (Article, Projet, Compétence…) en hérite.

Les vues avec Twig

La présentation est gérée par Twig : les contrôleurs ne produisent jamais de HTML directement, ils transmettent des données à un gabarit. Twig échappe automatiquement les variables, ce qui protège contre les failles XSS, et permet l'héritage de gabarits (un base.twig commun, des pages qui l'étendent).

Le back-office sur mesure

C'est le cœur du « CMS » : une zone /admin protégée par authentification, d'où l'on gère tout le contenu — sections, projets, compétences, parcours, articles de blog, messages et paramètres. Chaque type de contenu dispose d'un CRUD complet, avec éditeur de texte riche et upload d'images sécurisé.

Le principe : tout ce qui est visible sur le site doit être modifiable depuis l'administration, sans toucher au code.

La sécurité, dès la conception

  • Requêtes préparées partout (anti-injection SQL) ;
  • Jeton CSRF vérifié sur chaque formulaire POST ;
  • Mots de passe hachés avec password_hash (bcrypt) et session régénérée à la connexion ;
  • Upload contrôlé : type MIME réel vérifié, extensions limitées, taille plafonnée ;
  • Échappement automatique des sorties via Twig (anti-XSS).

Le modèle de données

Les tables restent simples et explicites. La relation la plus intéressante : une section peut être marquée « blog » et contenir alors plusieurs articles — exactement le mécanisme qui publie le billet que vous lisez.

Modèle de données : la table sections est reliée en un-à-plusieurs à la table articles ; autres tables : projects, skills, timeline_entries, users, messages, settings, social_links.
Une section « blog » (1) possède plusieurs articles (N). Le reste du contenu suit le même esprit.

En résumé

Construire un CMS maison, ce n'est pas réécrire un mastodonte : c'est assembler quelques briques bien comprises — front controller, routeur, couche données préparée, vues Twig, back-office — et garder la maîtrise totale du résultat. Le code reste petit, rapide, et fait exactement ce dont on a besoin. Pour un portfolio, c'est idéal ; et c'est aussi la meilleure façon d'apprendre ce qui se cache vraiment sous le capot des frameworks.