Objectif
- Dans ce tutoriel, vous allez :
- Comprendre la différence entre MVC et MVVM et pourquoi passer à MVVM.
- Remplacer les Controllers par des ViewModels qui encapsulent la logique de présentation.
- Exposer un objet
$vmentier à la vue, au lieu de variables extraites viaextract(). - Interagir avec une base de données MySQL via une classe Model centralisée.
- Dans ce tutoriel, vous allez :
Qu'est-ce que le MVVM ?
- MVVM = Model – View – ViewModel
- Pattern architectural créé par Microsoft (WPF/Silverlight) pour les interfaces riches. Il vise à dissocier totalement la logique métier de l'interface graphique.
Couche Rôle Model Données & logique métier (BDD, API, règles de validation) View Interface utilisateur (HTML/CSS, composants UI). Ne contient aucune logique. ViewModel Intermédiaire synchronisé. Expose les données formatées pour la Vue, gère l'état de l'UI, et traduit les actions utilisateur en commandes métier. MVC vs MVVM : la différence clé
- Le Concept : Le Buffet avec Assistant (MVVM)
- Imaginez que vous êtes dans un hôtel de luxe. Vous ne passez pas commande à un serveur qui disparaît en cuisine. Au lieu de cela, vous avez un Assistant (le ViewModel) qui se tient devant un Buffet (le Modèle) et qui prépare votre assiette en temps réel selon vos envies.
- 1. La Vue (Le Client)
- C'est vous. Vous regardez ce qui est disponible. Vous ne touchez pas directement à la nourriture dans les grands plats en argent. Vous interagissez avec l'assistant. Si vous dites "Je veux que ma viande soit plus cuite", vous ne parlez pas au chef, vous changez simplement votre préférence sur votre menu digital.
- 2. Le Modèle (La Cuisine / Les Stocks)
- C'est la source brute. Les ingrédients, les frigos, les recettes de base. Le Modèle ne sait pas qui vous êtes, il sait juste qu'il contient des steaks, des légumes et des sauces.
- 3. Le ViewModel (L'Assistant de table)
- C'est là que la magie opère. L'assistant fait le lien entre le buffet et vous de manière "automatique" :
- Data Binding (Liaison de données) : Si le chef ajoute un nouveau plat au buffet, l'assistant le met immédiatement à jour sur votre menu sans que vous ayez à demander "Quoi de neuf ?".
- Transformation : Le buffet propose du poisson entier (Donnée brute), mais l'assistant vous le présente en filets prêts à manger (Donnée formatée pour la Vue).
- État : Si vous cochez "Végétarien" sur votre menu, l'assistant masque instantanément tous les plats de viande pour vous.
- Le Concept : Le Buffet avec Assistant (MVVM)
MVC vs MVVM : la différence clé
- En MVC, le Controller reçoit la requête, appelle le Model, prépare les données et les pousse vers la View via
extract(). Il mélange logique HTTP et logique de présentation. - En MVVM, le ViewModel se charge uniquement de préparer et exposer les données pour la View. Le Router gère la requête HTTP. La View accède à un objet
$vmavec des propriétés typées et des méthodes utilitaires. - En MVC, le Controller orchestre et met à jour la Vue manuellement.
- En MVVM, le ViewModel utilise le Data Binding (souvent bidirectionnel) : la Vue se met à jour automatiquement quand le ViewModel change, et inversement, sans manipulation directe du DOM.
Aspect MVC (avant) MVVM (après) Classe centrale ControllerBaseViewModelDossier applicatif controllers/viewmodels/Méthode principale index(),show()…load(array $params)Données vers la View extract($data)→ variables libresObjet $vmavec propriétés typéesFormatage des données Dans la View ou le Controller Dans le ViewModel uniquement Rendu HTML render()dans le Controllerdispatch()dans le Router- En MVC, le Controller reçoit la requête, appelle le Model, prépare les données et les pousse vers la View via
Structure du Projet
- Voici l'organisation des fichiers pour ce projet MVVM :
mon-projet-mvvm/ ├── config/ │ └── database.php # Paramètres de connexion (inchangé) ├── core/ │ ├── BaseViewModel.php # Classe mère des ViewModels (remplace Controller.php) │ ├── Model.php # Connexion PDO partagée (inchangé) │ └── Router.php # Instancie les ViewModels au lieu des Controllers ├── viewmodels/ # Remplace controllers/ │ ├── UtilisateurViewModel.php │ └── ClientViewModel.php ├── models/ │ ├── Utilisateur.php │ └── Client.php ├── views/ │ ├── layouts/ │ │ ├── header.php # En-tête HTML commun │ │ └── footer.php # Pied de page commun │ ├── utilisateur/ │ │ └── index.php # Accède à $vm directement │ └── client/ │ └── index.php # Accède à $vm directement ├── public/ │ ├── index.php # Point d'entrée unique │ └── .htaccess # Réécriture d'URL
- Voici l'organisation des fichiers pour ce projet MVVM :
Rôle de chaque dossier & fichiers attendus
Dossier Rôle Fichiers types Remarques public/ Racine web (seul dossier accessible via navigateur) index.php, .htaccess, assets/css/, assets/js/ Configurez votre serveur web pour pointer uniquement ici. Bloquez l'accès direct aux autres dossiers. config/ Configuration de l'application database.php, .env Ne doit jamais être exposé. Inchangé par rapport au MVC. core/ Classes fondatrices du framework MVVM BaseViewModel.php, Model.php, Router.php Contient la logique transversale. BaseViewModelremplaceController.viewmodels/ Préparation et exposition des données pour les Views UtilisateurViewModel.php, ClientViewModel.php Un ViewModel par entité. Appelle le Model, transforme les données, expose des propriétés typées. Aucun HTML. models/ Accès aux données — données brutes uniquement Utilisateur.php, Client.php Plus épuré qu'en MVC : aucun formatage, aucune logique de présentation. Hérite de core/Model.php. views/ Couche présentation — liée au ViewModel layouts/header.php, layouts/footer.php, client/index.php Accède uniquement à $vm. Jamais de logique métier ni deextract().Bonnes pratiques essentielles
Point d'entrée unique & Sécurité
- Seul
public/index.phpest accessible via le navigateur.- Dans une architecture MVVM,
public/index.phpreste le point d'entrée unique. Son rôle : charger les dépendances, définir les constantes, et lancer le Router. - Détail rôle par rôle
<?php // Mode développement : afficher les erreurs ini_set('display_errors', 1); error_reporting(E_ALL); // Constantes de chemins define('ROOT_PATH', dirname(__DIR__) . '/'); define('BASE_URL', ''); // Ex: '/mon-projet-mvvm' si dans un sous-dossier // Chargement des couches core require_once ROOT_PATH . 'config/database.php'; require_once ROOT_PATH . 'core/Model.php'; require_once ROOT_PATH . 'core/BaseViewModel.php'; require_once ROOT_PATH . 'core/Router.php'; // Lancement du router MVVM $router = new Router( ROOT_PATH . 'views', ROOT_PATH . 'viewmodels' ); $router->dispatch();Bloc Pourquoi ? Bonne pratique define('ROOT_PATH', ...) Évite les chemins relatifs fragiles (../../) qui peuvent casser lors de l'inclusion de fichiers. Utilisez __DIR__etDIRECTORY_SEPARATORpour garantir la portabilité entre Windows et Linux.require_once BaseViewModel.php Charge la classe mère avant d'instancier un ViewModel concret dans le Router. Indispensable : le Router fait new $vmClass(), qui hérite de BaseViewModel.new Router(...) On passe les chemins vers views/etviewmodels/pour que le Router soit découplé de la structure de dossiers.Facilite le déplacement ou renommage des dossiers sans modifier le Router. $router->dispatch() Délègue toute la logique de routage au Router : parse l'URL, instancie le ViewModel, inclut la View. Le fichier index.phpreste léger : il ne route pas, ne se connecte pas à la DB directement. - Dans une architecture MVVM,
- Seul
- Le fichier
public/.htaccess.htaccess(Apache) doit réécrire toutes les requêtes versindex.php.- Contenu de public/.htaccess
RewriteEngine On RewriteBase / # Bloquer les fichiers cachés (.env, .git...) RewriteRule (^\.|/\.) - [F] # Servir directement les assets statiques (CSS, JS, images) RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d # Tout le reste → index.php RewriteRule ^(.*)$ index.php?url=$1 [QSA,L] Options -Indexes - Rôle ligne par ligne
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?url=$1 [QSA,L] Redirige toutes les requêtes vers index.php. QSA préserve les paramètres URL, et L 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 est absent. RewriteRule (^.|/.) - [F] Interdit l'accès (Forbidden) aux fichiers cachés commençant par un point (comme .envou.git).
core/BaseViewModel.php et core/Model.php
- core/BaseViewModel.php – Classe mère des ViewModels
<?php /** * Classe mère abstraite de tous les ViewModels. * * En MVVM, le ViewModel est le "pont" entre le Model (données) * et la View (présentation). Il expose des propriétés typées * et des méthodes de formatage — JAMAIS de HTML. */ abstract class BaseViewModel { /** * Chemin relatif de la vue dans views/ * Exemple : "client/index" */ protected string $viewPath = ''; /** * Charge les données nécessaires à la vue. * Appelé par le Router avant le rendu. * Peut recevoir des paramètres d'URL (ex: id). */ abstract public function load(array $params = []): void; /** * Retourne le chemin de la vue associée à ce ViewModel. */ public function getViewPath(): string { if (empty($this->viewPath)) { throw new RuntimeException( get_class($this) . ' doit définir $viewPath.' ); } return $this->viewPath; } } - core/Model.php – Classe mère des modèles (PDO sécurisé)
<?php require_once __DIR__ . '/../config/database.php'; abstract class Model { protected PDO $db; protected string $table; public function __construct() { $this->db = Database::getInstance(); } protected function query(string $sql, array $params = []): PDOStatement { $stmt = $this->db->prepare($sql); $stmt->execute($params); return $stmt; } protected function fetchAll(string $sql, array $params = []): array { return $this->query($sql, $params)->fetchAll(); } protected function fetch(string $sql, array $params = []): array|false { return $this->query($sql, $params)->fetch(); } }
- core/BaseViewModel.php – Classe mère des ViewModels
Fichier core/Router.php
- Rôle du Router dans le cycle MVVM
- Le routeur a 3 missions principales :
- Parser l'URL demandée et identifier le ViewModel correspondant (ex: /client →
ClientViewModel) - Instancier le ViewModel et appeler sa méthode
load()pour préparer les données - Inclure la View en lui passant l'objet
$vmentier (pas deextract())
- Parser l'URL demandée et identifier le ViewModel correspondant (ex: /client →
<?php class Router { private string $viewsPath; private string $viewModelsPath; public function __construct(string $viewsPath, string $viewModelsPath) { $this->viewsPath = $viewsPath; $this->viewModelsPath = $viewModelsPath; } public function dispatch(): void { // 1. Parser l'URL $url = trim($_GET['url'] ?? 'client', '/'); $parts = explode('/', $url); $resource = ucfirst(strtolower($parts[0])); // "client" → "Client" $params = array_slice($parts, 1); // ["5"] pour /client/5 // 2. Résoudre le ViewModel $vmClass = $resource . 'ViewModel'; // "ClientViewModel" $vmFile = $this->viewModelsPath . '/' . $vmClass . '.php'; if (!file_exists($vmFile) || !preg_match('/^[A-Za-z]+$/', $resource)) { http_response_code(404); echo "<p style='color:red'>❌ ViewModel '<strong>$vmClass</strong>' introuvable.</p>"; return; } require_once $vmFile; // 3. Instancier & charger le ViewModel $vm = new $vmClass(); $vm->load($params); // 4. Rendre la vue en lui passant $vm $viewFile = $this->viewsPath . '/' . $vm->getViewPath() . '.php'; if (!file_exists($viewFile)) { http_response_code(404); echo "<p style='color:red'>❌ Vue '<strong>$viewFile</strong>' introuvable.</p>"; return; } require_once ROOT_PATH . 'views/layouts/header.php'; require_once $viewFile; // $vm est disponible ici require_once ROOT_PATH . 'views/layouts/footer.php'; } } - Le routeur a 3 missions principales :
- Rôle du Router dans le cycle MVVM
Configuration & Noyau (Core)
1. Connexion Base de Données (config/database.php)
- Ce fichier est identique à la version MVC. Le changement de pattern n'affecte pas la couche de connexion.
-
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
<?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; } }
- Ce fichier est identique à la version MVC. Le changement de pattern n'affecte pas la couche de connexion.
2. Le ViewModel Parent (core/BaseViewModel.php)
- Rôle de core/BaseViewModel.php dans l'architecture MVVM
-
Router (dispatch)
↓
BaseViewModel (load + getViewPath) ← "Prépare les données, expose $vm"
↓
ViewModel concret (ClientViewModel) ← "Appelle le Model, formate les données"
↓
Model (Client.php) ← "Requêtes SQL pures"
↓
PDO → MySQL
- Rôle de core/BaseViewModel.php dans l'architecture MVVM
3. Le Contrôleur est supprimé
- En MVVM, le fichier
core/Controller.phpn'existe plus. Son rôle est réparti entre :- Le Router: gère la requête HTTP et le rendu de la View
- Le ViewModel: prépare et expose les données
- En MVVM, le fichier
Les ViewModels
- ViewModel (viewmodels/ClientViewModel.php) :
- ViewModel (viewmodels/UtilisateurViewModel.php) :
<?php require_once __DIR__ . '/../core/BaseViewModel.php'; require_once __DIR__ . '/../models/Client.php'; class ClientViewModel extends BaseViewModel { // ── Propriétés publiques exposées à la View ────────────── /** @var array Liste des clients formatés */ public array $clients = []; public int $totalCount = 0; public string $pageTitle = 'Liste des Clients'; public bool $isEmpty = false; protected string $viewPath = 'client/index'; public function load(array $params = []): void { $model = new Client(); // Récupération des données brutes $rawClients = $model->getAll(); // Transformation : on enrichit chaque client dans le ViewModel $this->clients = array_map( fn(array $c) => $this->formatClient($c), $rawClients ); $this->totalCount = count($this->clients); $this->isEmpty = $this->totalCount === 0; } private function formatClient(array $client): array { return [ ...$client, // Catégorie d'âge calculée ici, pas dans la View 'age_label' => $this->getAgeLabel((int)$client['age']), // Nom complet formaté 'full_name' => htmlspecialchars($client['name'] . ' ' . $client['family']), // Initiales pour un avatar CSS 'initials' => strtoupper(mb_substr($client['name'], 0, 1) . mb_substr($client['family'], 0, 1)), ]; } // Méthode publique appelable depuis la View : $vm->getAgeLabel(25) public function getAgeLabel(int $age): string { return match(true) { $age < 18 => 'Mineur', $age < 30 => 'Jeune adulte', $age < 50 => 'Adulte', default => 'Senior', }; } // Méthode publique : $vm->getAverageAge() public function getAverageAge(): float { if ($this->isEmpty) return 0.0; $total = array_sum(array_column($this->clients, 'age')); return round($total / $this->totalCount, 1); } }<?php require_once __DIR__ . '/../core/BaseViewModel.php'; require_once __DIR__ . '/../models/Utilisateur.php'; class UtilisateurViewModel extends BaseViewModel { public array $utilisateurs = []; public int $totalCount = 0; public bool $isEmpty = false; public string $pageTitle = 'Liste des Utilisateurs'; protected string $viewPath = 'utilisateur/index'; public function load(array $params = []): void { $model = new Utilisateur(); $raw = $model->getAll(); $this->utilisateurs = array_map( fn(array $u) => $this->formatUtilisateur($u), $raw ); $this->totalCount = count($this->utilisateurs); $this->isEmpty = $this->totalCount === 0; } private function formatUtilisateur(array $u): array { // Le décodage JSON des rôles est fait ICI, pas dans la View $roles = json_decode($u['roles'] ?? '[]', true) ?? []; return [ ...$u, 'roles_list' => $roles, 'roles_label' => implode(', ', $roles), 'is_admin' => in_array('ROLE_ADMIN', $roles), 'email_safe' => htmlspecialchars($u['email'] ?? ''), ]; } // Méthode publique : $vm->countAdmins() public function countAdmins(): int { return count(array_filter( $this->utilisateurs, fn(array $u) => $u['is_admin'] )); } }Les Modèles
- En MVVM, le Model est encore plus épuré qu'en MVC : il retourne des données brutes. Toute logique de présentation a migré vers le ViewModel.
- Modèle (models/Client.php) :
- Modèle (models/Utilisateur.php) :
<?php require_once __DIR__ . '/../core/Model.php'; /** * Model Client — Données brutes uniquement. * Pas de décodage JSON, pas de formatage : c'est le rôle du ViewModel. */ class Client extends Model { protected string $table = 'client'; public function getAll(): array { return $this->fetchAll( "SELECT id, name, family, age FROM {$this->table} ORDER BY id DESC" ); } public function findById(int $id): array|false { return $this->fetch( "SELECT * FROM {$this->table} WHERE id = ?", [$id] ); } }<?php require_once __DIR__ . '/../core/Model.php'; class Utilisateur extends Model { protected string $table = 'utilisateurs'; public function getAll(): array { return $this->fetchAll( "SELECT id, email, roles, password FROM {$this->table} ORDER BY id DESC" ); } public function findByEmail(string $email): array|false { return $this->fetch( "SELECT * FROM {$this->table} WHERE email = ?", [$email] ); } }views/layouts/header.php
- views/layouts/header.php
<!DOCTYPE html> <html lang="fr"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>MVVM - 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="<?= BASE_URL ?>/client">? Clients</a> <a href="<?= BASE_URL ?>/utilisateur">? Utilisateurs</a> </nav> <main style="max-width: 1000px; margin: 0 auto;">views/layouts/footer.php
</main> <footer style="text-align: center; margin-top: 40px; color: #868e96; font-size: 0.9em;"> © 2026 - Architecture MVVM PHP/MySQL </footer> </body> </html>views/utilisateur/index.php
- views/utilisateur/index.php
- Différence clé : la View accède à
$vmau lieu de variables extraites. Plus aucune variable "magique".
<?php /** @var UtilisateurViewModel $vm */ ?> <h2>? <?= htmlspecialchars($vm->pageTitle) ?></h2> <p>Total : <strong><?= $vm->totalCount ?></strong> utilisateur(s) — Admins : <strong><?= $vm->countAdmins() ?></strong></p> <table> <thead> <tr><th>ID</th><th>Email</th><th>Rôles</th><th>Admin ?</th></tr> </thead> <tbody> <?php if ($vm->isEmpty): ?> <tr><td colspan="4" class="empty">Aucun utilisateur enregistré.</td></tr> <?php else: ?> <?php foreach ($vm->utilisateurs as $u): ?> <tr> <td><?= htmlspecialchars((string)$u['id']) ?></td> <td><?= $u['email_safe'] ?></td> <td><code><?= htmlspecialchars($u['roles_label']) ?></code></td> <td><?= $u['is_admin'] ? '✅ Oui' : '—' ?></td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table>views/client/index.php
<?php /** @var ClientViewModel $vm */ ?> <h2>? <?= htmlspecialchars($vm->pageTitle) ?></h2> <p>Total : <strong><?= $vm->totalCount ?></strong> client(s) — Âge moyen : <strong><?= $vm->getAverageAge() ?> ans</strong></p> <table> <thead> <tr><th>ID</th><th>Nom complet</th><th>Âge</th><th>Catégorie</th></tr> </thead> <tbody> <?php if ($vm->isEmpty): ?> <tr><td colspan="4" class="empty">Aucun client enregistré.</td></tr> <?php else: ?> <?php foreach ($vm->clients as $client): ?> <tr> <td><?= htmlspecialchars((string)$client['id']) ?></td> <td><?= $client['full_name'] ?></td> <td><?= htmlspecialchars((string)$client['age']) ?> ans</td> <td><?= htmlspecialchars($client['age_label']) ?></td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table>Script SQL
- Le script SQL est identique à la version MVC — le changement de pattern n'affecte pas la structure de la base de données.
- Le mot de passe en clair correspondant à ce hash bcrypt (
$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi) est tout simplement : password.
-- ========================================== -- 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);Exercice : Gestion des Produits en MVVM PHP
- Objectifs pédagogiques
- À la fin de cet exercice, l'apprenant sera capable de :
- Comprendre l'architecture MVVM (Model – ViewModel – View) en PHP
- Créer une table MySQL
produits - Implémenter un Model pour interagir avec la base de données
- Créer un ViewModel pour préparer et formater les données
- Créer une View qui accède uniquement à l'objet
$vm - 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 MVVM.
- 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 la table produits
- Créer une table
produitsavec les champs suivants :idINT, PK, AI — Identifiant du produit |nomVARCHAR(150) — Nom du produit |descriptionTEXT — Description |prixDECIMAL(10,2) — Prix |stockINT — Quantité en stock |date_creationDATETIME — Date d'insertion- La table doit utiliser InnoDB et l'encodage UTF8.
- Insérer au moins 5 produits dans la table
produitspour effectuer vos tests. - Structure MVVM
- Vous disposez de l'architecture suivante :
mon-projet-mvvm/ │ ├── viewmodels/ │ └── ProduitViewModel.php │ ├── models/ │ └── Produit.php │ ├── views/ │ └── produit/ │ └── index.php │ ├── core/ │ └── BaseViewModel.php │ └── public/ └── index.php
- Création du Model
- Fichier :
models/Produit.php- Créer un Model
Produitqui hérite deModelet retourne des données brutes. - Le Model doit contenir au minimum une méthode :
getAll()
- Création du ViewModel
- Fichier :
viewmodels/ProduitViewModel.php- Créer un ViewModel
ProduitViewModelqui : - Hérite de
BaseViewModel - Définit
protected string $viewPath = 'produit/index' - Implémente la méthode
load()qui appelle le Model et formate les données - Expose les propriétés publiques :
$produits,$totalCount,$isEmpty,$pageTitle - Contient une méthode publique
formatPrix(float $prix): stringretournant le prix formaté en TND
- Créer un ViewModel
- Créer un Model
- Création de la View
- Fichier :
views/produit/index.php - Créer une vue qui :
- N'utilise que
$vm— aucune variable libre - Affiche le titre via
$vm->pageTitle - Affiche les produits sous forme de table HTML
- Affiche les colonnes :
- Nom
- Description
- Prix (via
$vm->formatPrix($produit['prix'])) - Stock
- Date de création
- Affiche les colonnes :
- Routage
- Configurer le routage pour que l'URL suivante fonctionne :
http://localhost/votre-projet/public/produit- Cette URL doit charger automatiquement :
ProduitViewModel(méthodeload())- Vue
produit/index.php
- Cette URL doit charger automatiquement :