Tutoriel MVC PHP MySQL
Architecture MVC Multi-Entités : Utilisateurs & Clients
-
Objectif
- Dans ce tutoriel, vous allez :
- Gérer plusieurs contrôleurs (Utilisateur et Client) au sein d’une même structure.
- Mettre en place un système de Layouts (Header/Footer) pour éviter la répétition de code.
- Développer un routeur dynamique capable de charger le bon contrôleur selon l’URL.
- Interagir avec une base de données MySQL via une classe Model centralisée.
-
Structure du Projet
- Voici l’organisation des fichiers pour ce projet évolutif :
-
Rôle de chaque dossier & fichiers attendus
-
Bonnes pratiques essentielles
-
Point d’entrée unique & Sécurité
- Seul
public/index.phpest accessible via le navigateur. - Dans une architecture MVC moderne, public/index.php est le point d’entrée unique (Front Controller). Il ne doit contenir aucune logique métier, ni routing, ni connexion DB. Son seul rôle est de préparer l’environnement, charger les dépendances et lancer le cœur de l’application.
- Voici une version professionnelle, sécurisée et commentée :
- Détail rôle par rôle
- Le fichier
public/.htaccess .htaccess(Apache) ounginx.confdoit réécrire toutes les requêtes versindex.php.- Désactivez display_errors en production. Utilisez un système de logs (storage/logs/).
- Contenu de public/.htaccess
- Rôle ligne par ligne
- Débriefing pédagogique (Questions / Réponses)
-
core/Controller.php et core/Model.php
- core/Controller.php – Classe mère des contrôleurs
- core/Model.php – Classe mère des modèles (PDO sécurisé)
-
Fichier core/Router.php et routers/web.php
- Rôle du Router dans le cycle MVC
- Le routeur a 3 missions principales :
- Parser l’URL demandée, quel que soit l’environnement (
localhost/sous-dossier/Virtual Host) - Faire correspondre cette URL à une route définie (ex: /client/5 →
ClientController@show) - Instancier et exécuter le bon contrôleur avec les bons paramètres
-
Configuration & Noyau (Core)
- 1. Connexion Base de Données (config/database.php)
- Rôle de config/database.php dans l’architecture MVC
- Ses 3 missions principales :
- 2. Le Modèle Parent (core/Model.php)
- Rôle de core/Model.php dans l’architecture MVC
- 2. Le Contrôleur Parent (core/Controller.php)
-
Les Modèles
- Modèle (models/Utilisateur.php) :
- Modèle (models/Client.php) :
- Contrôleur (controllers/UtilisateurController.php) :
- Contrôleur (ontrollers/ClientController.php) :
- views/layouts/header.php:
-
views/layouts/footer.php
-
views/utilisateur/index.php
- views/utilisateur/index.php
-
views/client/index.php
-
public/index.php
-
public/.htaccess
- views/client/index.php
-
Script SQL
- Script sql
- Le mot de passe en clair correspondant à ce hash bcrypt ($2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi) est tout simplement : password.
- Appel des ‘Client et Utilisateurs
- views/layouts/header.php
- ajouter .htaccess
- Changer public/index.php
- Changer public/.htaccess
- Changer public/index.php
-
Exercice : Gestion des Produits en MVC PHP
- Objectifs pédagogiques
- À la fin de cet exercice, l’apprenant sera capable de :
- Comprendre l’architecture MVC (Model – View – Controller) en PHP
- Créer une table MySQL produits
- Implémenter un model pour interagir avec la base de données
- Créer un controller pour gérer la logique métier
- Créer des views pour afficher les données
- Afficher la liste des produits depuis la base de données
- Contexte de l’exercice
- Vous travaillez dans une application web développée en PHP avec l’architecture MVC.
- On vous demande de créer un module Produits permettant d’afficher une liste de produits stockés dans une base de données MySQL.
- Création de la base de données
- Créer une table produits avec les champs suivants :
- ChampTypeDescriptionidINT, PK, AIIdentifiant du produitnomVARCHAR(150)Nom du produitdescriptionTEXTDescription du produitprixDECIMAL(10,2)Prix du produitstockINTQuantité en stockdate_creationDATETIMEDate d’insertion
- La table doit utiliser InnoDB et l’encodage UTF8.
- Insérer des données de test
- Insérer au moins 5 produits dans la table produits pour effectuer vos tests.
- Structure MVC
- Vous disposez de l’architecture suivante :
- Création du Model
- Fichier : app/Models/Produit.php
- Créer un model Produit qui permet de :
- Se connecter à la base de données
- Récupérer la liste de tous les produits
- Le model doit contenir au minimum une méthode :
getAllProduits() - Création du Controller
- Fichier : app/Controllers/ProduitController.php
- Créer un controller ProduitController qui :
- Hérite du controller principal
- Appelle le model Produit
- Récupère la liste des produits
- Envoie les données vers la vue
- Le controller doit contenir une méthode :
index() - Création de la View
- Fichier : app/Views/produits/index.php
- Créer une vue qui :
- Affiche le titre « Liste des produits »
- Affiche les produits sous forme de table HTML
- Affiche les colonnes :
- Nom
- Description
- Prix
- Stock
- Date de création
- Routage
- Configurer le routage pour que l’URL suivante fonctionne :
- http://localhost/votre-projet/public/produit
- Cette URL doit appeler :
- ProduitController
- Méthode index
- Vue produits/index.php
mon-projet-mvc/
├── config/
│ └── database.php # Paramètres de connexion
├── core/
│ ├── Controller.php # Classe mère des contrôleurs
│ └── Model.php # Connexion PDO partagée
├── controllers/
│ ├── UtilisateurController.php
│ └── ClientController.php
├── models/
│ ├── Utilisateur.php
│ └── Client.php
├── views/
│ ├── layouts/
│ │ ├── header.php # En-tête HTML commun
│ │ └── footer.php # Pied de page commun
│ ├── utilisateur/
│ │ └── index.php # Liste des utilisateurs
│ └── client/
│ └── index.php # Liste des clients
├── public/
│ ├── index.php # Routeur principal
│ └── .htaccess # Réécriture d'URL
| Dossier | Rôle | Fichiers types | Remarques |
|---|---|---|---|
| public/ | Racine web (seul dossier accessible via navigateur) | index.php, .htaccess, assets/css/, assets/js/, assets/img/ | Configurez votre serveur web pour pointer uniquement ici. Bloquez l’accès direct aux autres dossiers. |
| config/ | Configuration de l’application | database.php, app.php, routes.php, .env | Ne doit jamais être exposé. Utilisez des variables d’environnement (.env) et un loader sécurisé. |
| core/ (ou src/) | Classes fondatrices du framework MVC | Controller.php, Model.php, Router.php, Database.php, App.php | Contient la logique transversale. Ne doit pas contenir de logique métier spécifique. |
| controllers/ | Gestion des requêtes HTTP & coordination | UtilisateurController.php, ClientController.php, AuthController.php | Un contrôleur par fonctionnalité. Reçoit la requête, appelle le modèle, passe les données à la vue. |
| models/ | Accès aux données & règles métier | Utilisateur.php, Client.php, Product.php | Contient les requêtes DB, validation, transactions. Aucun HTML/echo. Hérite généralement de core/Model.php. |
| views/ | Couche présentation (templates) | layouts/header.php, layouts/footer.php, utilisateur/index.php, client/index.php | Organisation par contrôleur. Ne contient que du HTML + affichage sécurisé de variables. Pas de logique métier. |
| storage/ (optionnel) | Fichiers générés à l’exécution | logs/app.log, cache/, uploads/ | À exclure du versionnage (.gitignore). Permissions restrictives. |
| vendor/ | Dépendances Composer | autoload.php, bibliothèques tierces | Généré automatiquement. Ne pas versionner. |
<?php
// public/index.php
// 1️⃣ Définir les constantes de chemins (fiables, indépendantes du serveur)
define('ROOT_PATH', dirname(__DIR__) . DIRECTORY_SEPARATOR);
define('PUBLIC_PATH', __DIR__ . DIRECTORY_SEPARATOR);
// 2️⃣ Charger l'autoloader Composer (gestion automatique des classes & namespaces)
require ROOT_PATH . 'vendor/autoload.php';
// 3️⃣ Configuration de l'environnement
// En production : display_errors = 0 | En développement : 1
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', ROOT_PATH . 'storage/logs/app.log');
error_reporting(E_ALL);
date_default_timezone_set('Europe/Paris');
// 4️⃣ (Optionnel mais recommandé) Charger les variables d'environnement
// if (file_exists(ROOT_PATH . '.env')) {
// $dotenv = Dotenv\Dotenv::createImmutable(ROOT_PATH);
// $dotenv->load();
// }
// 5️⃣ Exécution de l'application avec gestion globale des exceptions
try {
// Instanciation du cœur de l'application
$app = new App\Core\App();
// Lancement du cycle de vie : config → routeur → contrôleur → vue
$app->run();
} catch (Throwable $e) {
// 🛡️ En production : on logue et on affiche une page 500 propre
error_log($e->getMessage() . ' | Fichier: ' . $e->getFile() . ' | Ligne: ' . $e->getLine());
http_response_code(500);
// Affichage basique si le moteur de vue n'est pas encore chargé
if (defined('ROOT_PATH')) {
include ROOT_PATH . 'views/errors/500.php';
} else {
echo '<h1>Erreur Serveur Interne</h1><p>Veuillez réessayer plus tard.</p>';
}
}
| Bloc | Pourquoi ? | Bonne pratique |
|---|---|---|
| define(‘ROOT_PATH’, …) | Évite les chemins relatifs fragiles (../../) qui peuvent casser lors de l’inclusion de fichiers. | Utilisez __DIR__ et la constante DIRECTORY_SEPARATOR pour garantir la portabilité entre Windows et Linux. |
| vendor/autoload.php | Charge automatiquement les classes (core, controllers, models) via la norme PSR-4. | Indispensable pour la maintenance. Élimine l’utilisation de require ou include manuels à travers le projet. |
| ini_set() & error_reporting() | Permet de contrôler finement la visibilité et le niveau de détail des erreurs PHP. | Désactivez display_errors (0) en production. Redirigez les erreurs vers des fichiers logs dans storage/logs/. |
| try/catch (Throwable) | Capture les erreurs fatales, les exceptions et les warnings qui n’ont pas été gérés par le code. | En cas d’échec, affichez une page d’erreur 500 propre à l’utilisateur et enregistrez l’erreur détaillée pour le débogage. |
| $app->run() | Délègue toute la logique d’exécution au cœur du framework MVC. | Le fichier index.php doit rester léger : il ne doit ni router, ni se connecter à la DB, ni charger de vue directement. |
# public/.htaccess
RewriteEngine On
RewriteBase /
# 🛡️ Bloquer l'accès direct aux fichiers/dossiers cachés (.env, .git, .DS_Store, etc.)
RewriteRule (^\.|/\.) - [F]
# 📁 Si la requête pointe vers un fichier ou dossier réel, on le sert directement
# (CSS, JS, images, fonts dans public/assets/)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# 🔄 Sinon, on redirige TOUTE autre requête vers le point d'entrée PHP
RewriteRule ^ index.php [QSA,L]
# 🔒 Désactiver le listage de dossiers
Options -Indexes
# 📦 Headers de sécurité (optionnel mais fortement recommandé)
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "DENY"
Header set Referrer-Policy "strict-origin-when-cross-origin"
| Directive | Utilité |
|---|---|
| RewriteEngine On | Active le module de réécriture d’URL d’Apache, indispensable pour transformer les URLs virtuelles en requêtes traitables par PHP. |
| RewriteCond !-f / !-d | Vérifie que la requête ne correspond pas à un fichier (-f) ou un dossier (-d) existant. Cela permet d’accéder directement aux images, CSS et JS sans passer par le routeur. |
| RewriteRule ^ index.php [QSA,L] | Redirige toutes les requêtes vers index.php. QSA (Query String Append) préserve les paramètres URL (ex: ?id=5), et L (Last) indique qu’il s’agit de la dernière règle à appliquer. |
| Options -Indexes | Mesure de sécurité majeure : empêche Apache d’afficher la liste des fichiers d’un dossier si le fichier d’index (index.php/html) est absent. |
| RewriteRule (^.|/.) – [F] | Interdit l’accès (Forbidden) aux fichiers cachés commençant par un point (comme .env ou .git), protégeant ainsi vos informations sensibles. |
| Question | Réponse attendue | Exemple / Illustration |
|---|---|---|
| Pourquoi [L] est indispensable ? | Il stoppe l’exécution des règles suivantes. Sans lui, Apache pourrait réécrire l’URL en boucle ou appliquer d’autres règles non désirées. | Si vous avez une règle A qui transforme l’URL et une règle B juste après, sans [L], Apache tentera d’appliquer B sur le résultat de A. |
| Que fait QSA (Query String Append) ? | Il préserve et ajoute les paramètres ?clé=valeur de l’URL originale à la redirection vers index.php. Indispensable pour les formulaires GET, filtres, pagination. | Une URL /produits?page=2 est réécrite en index.php?page=2. Sans QSA, on recevrait juste index.php (donnée perdue). |
| Pourquoi !-f et !-d avant la réécriture ? | Pour éviter que le routeur PHP ne prenne le contrôle sur des assets statiques (CSS, JS, images), ce qui tuerait les performances et casserait l’affichage. | Si l’utilisateur demande /style.css, Apache le sert directement. Sans ces lignes, il chercherait un contrôleur « Style » dans votre code PHP. |
| Pourquoi .htaccess dans public/ et pas à la racine ? | Le DocumentRoot du serveur doit pointer vers public/. Ainsi, config/, core/, .env sont physiquement hors d’atteinte HTTP. Aucune règle .htaccess ne peut remplacer une mauvaise architecture de dossiers. | En pointant sur public/, une requête vers monsite.com/.env renverra une erreur 404 car le fichier est « au-dessus » dans l’arborescence. |
| Comment savoir si mod_rewrite est actif ? | apache2ctl -M | grep rewrite ou tester avec une règle simple et voir si elle s’applique. Sinon, activer via a2enmod rewrite + redémarrer Apache. |
Si vous écrivez n’importe quoi dans le .htaccess et que le site ne plante pas en « Internal Server Error 500 », c’est que le module (ou le fichier) est ignoré. |
<?php
namespace App\Core;
class Controller
{
/**
* Affiche une vue avec un layout commun (header + footer)
*
* @param string $view Chemin relatif dans views/ (ex: "client/index")
* @param array $data Variables à passer à la vue
*/
protected function render(string $view, array $data = []): void
{
// Rend les variables disponibles dans la vue (ex: $users, $title)
extract($data);
// Inclusion du layout et de la vue
require_once ROOT_PATH . 'views/layouts/header.php';
require_once ROOT_PATH . "views/{$view}.php";
require_once ROOT_PATH . 'views/layouts/footer.php';
// ⛔ STOPPE l'exécution pour éviter d'afficher du code après la vue
exit;
}
/**
* Redirection HTTP propre
*/
protected function redirect(string $url, int $code = 302): void
{
header("Location: {$url}", true, $code);
exit;
}
/**
* Retourne une réponse JSON (utile pour AJAX/API)
*/
protected function json(mixed $data, int $code = 200): void
{
header('Content-Type: application/json', true, $code);
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
exit;
}
}
db = self::getConnection();
}
/**
* Retourne une connexion PDO unique et partagée
*/
private static function getConnection(): PDO
{
if (self::$pdoInstance === null) {
try {
// 📦 Récupère la config (à adapter selon votre système de chargement)
$host = $_ENV['DB_HOST'] ?? '127.0.0.1';
$dbname = $_ENV['DB_NAME'] ?? 'mvc_app';
$username = $_ENV['DB_USER'] ?? 'root';
$password = $_ENV['DB_PASS'] ?? '';
$charset = $_ENV['DB_CHARSET'] ?? 'utf8mb4';
$dsn = "mysql:host={$host};dbname={$dbname};charset={$charset}";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // ⚠️ Lève des exceptions sur erreur SQL
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // Retourne des tableaux associatifs
PDO::ATTR_EMULATE_PREPARES => false, // ⚠️ Utilise les vraies requêtes préparées (sécurité)
PDO::ATTR_PERSISTENT => false,
];
self::$pdoInstance = new PDO($dsn, $username, $password, $options);
} catch (PDOException $e) {
// 🔒 Ne jamais exposer l'erreur brute en production
error_log('DB Connection Error: ' . $e->getMessage());
throw new PDOException('Impossible de se connecter à la base de données.', 0, $e);
}
}
return self::$pdoInstance;
}
/**
* Exécute une requête préparée et retourne le statement
*/
protected function query(string $sql, array $params = []): \PDOStatement
{
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt;
}
/**
* Retourne UN seul résultat
*/
protected function fetch(string $sql, array $params = []): array|false
{
return $this->query($sql, $params)->fetch();
}
/**
* Retourne TOUS les résultats
*/
protected function fetchAll(string $sql, array $params = []): array
{
return $this->query($sql, $params)->fetchAll();
}
}
-
Application PHP
↓
config/database.php ← « Où est la BDD ? Quels sont les accès ? »
↓
core/Model.php ← « Comment se connecter et exécuter des requêtes ? »
↓
PDO (PHP Data Objects) ← Driver de connexion réel
↓
MySQL / MariaDB
| Mission | Description | Pourquoi c’est crucial |
|---|---|---|
| 🔐 Centraliser les credentials | Host, dbname, user, pass dans un seul endroit | Évite de disperser les mots de passe dans tout le code |
| ♻️ Gérer une connexion unique | Pattern Singleton → une seule instance PDO pour toute l’app | Évite les fuites de connexions, améliore les performances |
| ⚙️ Configurer PDO proprement | Options de sécurité, encodage, mode d’erreur | Bloque les injections SQL, garantit la cohérence des données |
<?php
class Database {
private static ?PDO $instance = null;
private function __construct() {
$host = '127.0.0.1';
$db = 'base_test';
$user = 'root';
$pass = '';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
self::$instance = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
error_log($e->getMessage());
die('Erreur de connexion à la base de données.');
}
}
public static function getInstance(): PDO {
if (self::$instance === null) {
new self();
}
return self::$instance;
}
}
-
Contrôleur (ClientController)
↓
Modèle (Client extends Model) ← « Je veux récupérer des clients »
↓
core/Model.php (abstrait) ← « Voici comment se connecter et exécuter des requêtes »
↓
config/database.php (Database::getInstance())
↓
PDO → MySQL
<?php
require_once __DIR__ . '/../config/database.php';
abstract class Model {
protected PDO $db;
protected string $table;
public function __construct() {
$this->db = Database::getInstance();
}
}
<?php
/**
* Classe mère abstraite de tous les contrôleurs
* Fournit les méthodes de rendu, redirection et réponse JSON
*/
abstract class Controller {
protected function render(string $view, array $data = []): void {
extract($data);
$viewPath = __DIR__ . '/../views/' . $view . '.php';
if (!file_exists($viewPath)) {
http_response_code(404);
die("Vue introuvable : $view");
}
require_once $viewPath;
}
}
<?php
require_once __DIR__ . '/../core/Model.php';
class Utilisateur extends Model {
protected string $table = 'utilisateurs';
public function getAll(): array {
$stmt = $this->db->query("SELECT id, email, roles, password FROM {$this->table} ORDER BY id DESC");
return $stmt->fetchAll();
}
}
<?php
require_once __DIR__ . '/../core/Model.php';
class Client extends Model {
protected string $table = 'client';
public function getAll(): array {
$stmt = $this->db->query("SELECT id, name, family, age FROM {$this->table} ORDER BY id DESC");
return $stmt->fetchAll();
}
}
<?php
require_once __DIR__ . '/../core/Controller.php';
require_once __DIR__ . '/../models/Utilisateur.php';
class UtilisateurController extends Controller {
public function index(): void {
$model = new Utilisateur();
$data = $model->getAll();
// Décodage sécurisé du JSON pour l'affichage
foreach ($data as &$row) {
$row['roles_decoded'] = json_decode($row['roles'], true) ?? [];
}
$this->render('utilisateur/index', ['utilisateurs' => $data]);
}
}
<?php
require_once __DIR__ . '/../core/Controller.php';
require_once __DIR__ . '/../models/Client.php';
class ClientController extends Controller {
public function index(): void {
$model = new Client();
$clients = $model->getAll();
$this->render('client/index', ['clients' => $clients]);
}
}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MVC - Base Test</title>
<style>
body { font-family: system-ui, sans-serif; margin: 0; padding: 20px; background: #f8f9fa; }
nav { background: #212529; padding: 12px; margin-bottom: 25px; border-radius: 8px; }
nav a { color: #fff; text-decoration: none; margin-right: 18px; font-weight: 500; }
nav a:hover { text-decoration: underline; color: #adb5bd; }
table { width: 100%; border-collapse: collapse; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 6px; overflow: hidden; }
th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #e9ecef; }
th { background: #0d6efd; color: white; }
.empty { text-align: center; padding: 20px; color: #6c757d; }
code { background: #f1f3f5; padding: 2px 5px; border-radius: 4px; font-size: 0.9em; }
</style>
</head>
<body>
<nav>
<a href="/client">👥 Clients</a>
<a href="/utilisateur">🔐 Utilisateurs</a>
</nav>
<main style="max-width: 1000px; margin: 0 auto;">
-
<li>Ce fichier analyse l’URL pour instancier le bon contrôleur. Exemple d’URL :
views/layouts/footer.php
</main>
<footer style="text-align: center; margin-top: 40px; color: #868e96; font-size: 0.9em;">
© 2026 - Architecture MVC PHP/MySQL
</footer>
</body>
</html>
<?php require_once __DIR__ . '/../layouts/header.php'; ?>
<h2>📋 Liste des Utilisateurs</h2>
<table>
<thead>
<tr><th>ID</th><th>Email</th><th>Rôles</th><th>Password</th></tr>
</thead>
<tbody>
<?php if (!empty($utilisateurs)): ?>
<?php foreach ($utilisateurs as $u): ?>
<tr>
<td><?= htmlspecialchars((string)$u['id']) ?></td>
<td><?= htmlspecialchars($u['email']) ?></td>
<td><?= htmlspecialchars(implode(', ', $u['roles_decoded'])) ?></td>
<td><code><?= htmlspecialchars($u['password']) ?></code></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="4" class="empty">Aucun utilisateur enregistré.</td></tr>
<?php endif; ?>
</tbody>
</table>
<?php require_once __DIR__ . '/../layouts/footer.php'; ?>
<?php require_once __DIR__ . '/../layouts/header.php'; ?>
<h2>📋 Liste des Clients</h2>
<table>
<thead>
<tr><th>ID</th><th>Nom</th><th>Famille</th><th>Âge</th></tr>
</thead>
<tbody>
<?php if (!empty($clients)): ?>
<?php foreach ($clients as $c): ?>
<tr>
<td><?= htmlspecialchars((string)$c['id']) ?></td>
<td><?= htmlspecialchars($c['name']) ?></td>
<td><?= htmlspecialchars($c['family']) ?></td>
<td><?= htmlspecialchars((string)$c['age']) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="4" class="empty">Aucun client enregistré.</td></tr>
<?php endif; ?>
</tbody>
</table>
<?php require_once __DIR__ . '/../layouts/footer.php'; ?>
<?php
// Mode développement : afficher les erreurs
ini_set('display_errors', 1);
error_reporting(E_ALL);
// Chargement de la connexion BDD
require_once __DIR__ . '/../config/database.php';
// Parsing de l'URL
$url = $_GET['url'] ?? 'client';
$url = trim($url, '/');
$parts = explode('/', $url);
// Résolution dynamique contrôleur / méthode
$controllerName = ucfirst(strtolower($parts[0])) . 'Controller';
$methodName = isset($parts[1]) ? strtolower($parts[1]) : 'index';
$controllerFile = __DIR__ . "/../controllers/$controllerName.php";
// Vérification de sécurité & exécution
if (file_exists($controllerFile) && preg_match('/^[A-Za-z]+$/', $controllerName)) {
require_once $controllerFile;
$controller = new $controllerName();
if (method_exists($controller, $methodName) && preg_match('/^[A-Za-z]+$/', $methodName)) {
$controller->$methodName();
} else {
http_response_code(404);
echo "❌ Méthode '<strong>$methodName</strong>' introuvable dans $controllerName";
}
} else {
http_response_code(404);
echo "❌ Contrôleur '<strong>$controllerName</strong>' introuvable";
}
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
-- ==========================================
-- 1. CRÉATION DE LA BASE DE DONNÉES
-- ==========================================
CREATE DATABASE IF NOT EXISTS `base_test`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE `base_test`;
-- ==========================================
-- 2. CRÉATION DES TABLES
-- ==========================================
-- Table : utilisateurs
CREATE TABLE IF NOT EXISTS `utilisateurs` (
`id` INT NOT NULL AUTO_INCREMENT,
`email` VARCHAR(180) NOT NULL,
`roles` JSON NOT NULL,
`password` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UNIQ_EMAIL` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table : client
CREATE TABLE IF NOT EXISTS `client` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`family` VARCHAR(100) NOT NULL,
`age` INT NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci AUTO_INCREMENT=9;
-- ==========================================
-- 3. DONNÉES DE TEST
-- ==========================================
-- Utilisateurs (mots de passe hashés avec bcrypt, valeur test = 'password')
INSERT INTO `utilisateurs` (`email`, `roles`, `password`) VALUES
('admin@exemple.com', '["ROLE_ADMIN", "ROLE_USER"]', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'),
('user@exemple.com', '["ROLE_USER"]', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi');
-- Clients (8 entrées pour que le prochain AUTO_INCREMENT soit bien 9)
INSERT INTO `client` (`name`, `family`, `age`) VALUES
('Jean', 'Dupont', 34),
('Marie', 'Lefèvre', 28),
('Karim', 'Benali', 41),
('Sophie', 'Martin', 25),
('Lucas', 'Bernard', 30),
('Emma', 'Petit', 22),
('Hugo', 'Robert', 45),
('Léa', 'Moreau', 37);
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MVC - Base Test</title>
<style>
body { font-family: system-ui, sans-serif; margin: 0; padding: 20px; background: #f8f9fa; }
nav { background: #212529; padding: 12px; margin-bottom: 25px; border-radius: 8px; }
nav a { color: #fff; text-decoration: none; margin-right: 18px; font-weight: 500; }
nav a:hover { text-decoration: underline; color: #adb5bd; }
table { width: 100%; border-collapse: collapse; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 6px; overflow: hidden; }
th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #e9ecef; }
th { background: #0d6efd; color: white; }
.empty { text-align: center; padding: 20px; color: #6c757d; }
code { background: #f1f3f5; padding: 2px 5px; border-radius: 4px; font-size: 0.9em; }
.btn-edit, .btn-delete {
display: inline-block;
padding: 5px 10px;
border-radius: 4px;
text-decoration: none;
font-size: 0.85em;
margin-right: 5px;
}
.btn-edit { background: #ffc107; color: #000; }
.btn-delete { background: #dc3545; color: #fff; }
.btn-edit:hover { background: #e0a800; }
.btn-delete:hover { background: #b02a37; }
</style>
</head>
<body>
<nav>
<a href="<?= BASE_URL ?>/client">👥 Clients</a>
<a href="<?= BASE_URL ?>/utilisateur">🔐 Utilisateurs</a>
</nav>
<main style="max-width: 1000px; margin: 0 auto;"></main>
RewriteEngine On
RewriteBase /mon-projet-mvc/
RewriteRule ^$ public/index.php [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ public/index.php?url=$1 [QSA,L]
<?php
// Mode développement : afficher les erreurs
ini_set('display_errors', 1);
error_reporting(E_ALL);
// Chargement de la connexion BDD
require_once __DIR__ . '/../config/database.php';
define('BASE_URL', '/mon-projet-mvc');
// Parsing de l'URL
$url = $_GET['url'] ?? 'client';
$url = trim($url, '/');
$parts = explode('/', $url);
$GLOBALS['url_parts'] = $parts;
// Résolution dynamique contrôleur / méthode
$controllerName = ucfirst(strtolower($parts[0])) . 'Controller';
$methodName = isset($parts[1]) ? strtolower($parts[1]) : 'index';
$controllerFile = __DIR__ . "/../controllers/$controllerName.php";
// Vérification de sécurité & exécution
if (file_exists($controllerFile) && preg_match('/^[A-Za-z]+$/', $controllerName)) {
require_once $controllerFile;
$controller = new $controllerName();
if (method_exists($controller, $methodName) && preg_match('/^[A-Za-z]+$/', $methodName)) {
$controller->$methodName();
} else {
http_response_code(404);
echo "❌ Méthode '<strong>$methodName</strong>' introuvable dans $controllerName";
}
} else {
http_response_code(404);
echo "❌ Contrôleur '<strong>$controllerName</strong>' introuvable";
}
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
db->query("SELECT id, nom, prenom, age, email, telephone FROM {$this->table} ORDER BY id DESC");
return $stmt->fetchAll();
}
public function getById(int $id): array|false {
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function update(int $id, array $data): void {
$stmt = $this->db->prepare("UPDATE {$this->table} SET nom=?, prenom=?, age=?, email=?, telephone=? WHERE id=?");
$stmt->execute([$data['nom'], $data['prenom'], $data['age'], $data['email'], $data['telephone'], $id]);
}
public function delete(int $id): void {
$stmt = $this->db->prepare("DELETE FROM {$this->table} WHERE id = ?");
$stmt->execute([$id]);
}
}
Créer la table produits
app/
│
├── Controllers/
│ └── ProduitController.php
│
├── Models/
│ └── Produit.php
│
├── Views/
│ └── produits/
│ └── index.php
│
core/
│ └── Controller.php
│
public/
│ └── index.php
