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 :
-
Configuration & Noyau (Core)
- 1. Connexion Base de Données (config/database.php)
- 2. Le Modèle Parent (core/Model.php)
- 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.
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
<?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;
}
}
<?php
require_once __DIR__ . '/../config/database.php';
abstract class Model {
protected PDO $db;
protected string $table;
public function __construct() {
$this->db = Database::getInstance();
}
}
<?php
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);
