Apprendre PHP & MySQL : Design Pattern : MVC

Le design pattern MVC est le plus populaire dans la création de plateformes web. Il consiste à organiser son code source selon 3 pilliers : les modèles, les vues et les contrôleurs.

Icône de calendrier
Intermédiaire
12 chapitres

Qu’est-ce qu’un design pattern ?

Problématique : comment structurer son code source ?

La communication entre un client (navigateur de l’utilisateur) et un serveur s’effectue via le protocole HTTP ou HTTPS (version sécurisée).

Cette communication bilatérale se traduit par :

  1. L’envoi d’une requête HTTP du client vers le serveur
  2. L’envoi d’une réponse HTTP du serveur vers le client

Dans le cas d’un site web statique, la requête contient comme seule information le nom de la ressources à retourner. De son côté le serveur se contentera alors de retourner la ressource HTML statique en question.

Le traitement demandé au serveur est simple et ne demandera pas de gros travaux d’architecture du code source.

Dans le cas d’un site web dynamique, le serveur sera en charge de construire la réponse HTTP adaptée à la demande du client. La construction de cette réponse est d’autant plus complexe qu’elle peut nécessiter de réaliser de nombreuses actions en amont sur le serveur. En voici un flux d’exécution type :

  1. Analyse de la requête (méthode HTTP, paramètres GET, POST…)
  2. Exécution intermédiaire de scripts (interactions avec la BDD, manipulation de fichiers, envoi de mail…)
  3. Construction dynamique de la réponse (template .php => template .html)

Sans organisation, un site dynamique peut rapidement devenir chaotique en termes de structure de développement. Au risque de nous retrouver avec un code déstructuré et difficile à maintenir, le code source présent sur notre serveur aura besoin d’une certaine organisation.

Bref, il nous faut un design pattern.

Réponse : les design patterns

À la conception d’une application/d’un site web, les développeurs sont confrontés à des difficultés.

Les design patterns, ou « patrons de conception », sont des solutions éprouvées à ces problèmes récurrents. Ils permettent d’améliorer l’utilisabilité du site ou de l’application en définissant un ensemble de bonnes pratiques, de règles, testées et approuvées par des professionnels.

Lorsqu’une solution a été utilisée suffisamment souvent pour avoir fait ses preuves, on peut la proposer sous la forme d’un design pattern. Cela évite aux développeurs de réinventer la roue en créant quelque chose à partir de rien. C’est le cas du design pattern MVC.

Design pattern MVC

Le design pattern MVC qui définit un ensemble de règles architecturales et une nomenclature (conventions de nommage, casse, etc.) pour le développement d’un site web dynamique.

MVC signifie « Modèle - Vue - Contrôleur ». Il s’agit du design pattern le plus largement répandu sur le web, dont le rôle est de séparer la logique du code en trois parties que l’on retrouve dans des dossiers et fichiers spécifiques.

Modèles (M)

Les modèles sont responsables de la structure et des traitements effectués sur les données d’une application.

Les modèles regroupent à la fois :

  • Les entités (uniquement en POO) : les entités désignent la structure des données à manipuler (classes/objets mappées avec les tables de la base de données)
  • Les managers : leur rôle est d’interagir avec la base de données pour en écrire et lire des données (requêtes SQL d’insertion, modification, suppression, récupération) #CRUD. Ils agissent comme une couche d’abstraction qui va permettre au contrôleur d’interagir avec la BDD, sans se soucier du comment cela va être fait.

Vues (V)

Le rôle des vues est de rassembler au sein de templates les éléments structurels de nos pages web. Autrement dit, l’ensemble du code HTML du site web.

Très souvent les vues intègrent des petites portions de code PHP tels que l’affichage de variables, des boucles, des conditions, etc., venant dynamiser les templates.

Ces templates se divisent en 3 familles principales :

  1. Le layout : il va contenir le squelette commun à l’ensemble des pages web. Il contient les balises de premier niveau (<html>, <head> et <body>).
  2. Les pages : elles vont regrouper le contenu propre à chaque page web.
  3. Les partials : ils vont contenir les éléments d’interface communs à plusieurs pages web (navbar, footer…).

Contrôleurs (C)

Le rôle d’un contrôleur est de coordonner les actions à mettre en place pour retourner au client une réponse HTTP adaptée.

Pour cela, il travaille en étroite collaboration avec les modèles (données) et les vues (templates). Il joue en quelque sorte le rôle de chef d’orchestre du site.

Dans une application traditionnelle (un site web et non une API), cette réponse retournée par le serveur se traduit généralement par :

  • Une page web
  • Une redirection

Schéma bilan

Le schéma type du traitement d’une requête client par un serveur basé sur le design pattern MVC se résume à :

1. Recevoir la requête (Contrôleur)

Le contrôleur reçoit la requête HTTP.

2. Traiter la requête (Contrôleur + Modèles + Vues)
  1. Le contrôleur peut exécuter des tâches en tout genre (calcul, envoi de mail…).
  2. Le contrôleur peut interragir avec la BDD.
  3. Le contrôleur charge le template requis en lui injectant des données dynamique si nécessaire.
3. Renvoyer une réponse (Contrôleur)

Le contrôleur retourne une réponse HTTP (pag web / redirection)

Schéma - Design Pattern MVC

Cas pratique : créer une architecture MVC

Il n’existe pas une seule et unique implémentation du MVC en PHP. Il n’y a pas une architecture type à respecter scrupuleusement, la seule règle étant de découper son code en dossiers et fichiers appliquant cette logique de séparation des modèles, vues et contrôleurs.

Des architectures MVC diverses, bien que divisées en dossiers et fichiers différents, se ressembleront toujours dans les grandes lignes.

Il existe d’ailleurs de nombreux frameworks PHP basés sur le design pattern MVC comme Symfony ou encore Laravel, et ils ne l’implémentent pas tous deux de la même manière.

Nous allons voir ici une possibilité d’architecture pour implémenter notre propre architecture MVC maison.

Développons les mécaniques globales d’un petit blog de dev web en nous basant sur l’architecture MVC.

index.php : le point d’entrée

Lorsqu’un utilisateur navigue sur un site web, il envoie une requête HTTP au serveur qui va traiter sa demande.

  • Si j’accède à l’URL localhost/blog-dev/home.php, je demande au serveur de me retourner la page d’accueil.
  • Si j’accède à l’URL localhost/blog-dev/privacy_policy.php, je demande au serveur de me retourner la page de politique de confidentialité.

Et bien ça, c’était avant. Pourquoi ? Car cela impliquait de dupliquer dans chacune de nos pages les balises HTML de premier niveau (<html>, <head> et <body>), ainsi que la structure globale de l’application (header, footer…).

Désormais, pour factoriser le squelette de nos pages web, tous nos appels au serveur passeront par le fichier 📄 index.php situé à la racine du serveur. C’est l’unique point d’entrée de notre application.

Ce dernier va appeler le routeur de notre application avec la fonction require(), afin de lui déléguer la suite du traitement.

index.php
copié !
require('./router.php');

Routeur

Le rôle du routage est de savoir quelle page ou quelle action l’utilisateur souhaite en analysant l’URL et ses paramètres.

Lorsqu’une requête de l’utilisateur pointera 📄 index.php, il faudra désormais faire en sorte de lui faire passer un paramètre de requête HTTP GET afin de spécifier ce que veut l’utilisateur. C’est le fondement de notre routage.

  • Par exemple, l’URL localhost/blog-dev/index.php?page=home demanderait la page d’accueil.
  • Par exemple, l’URL localhost/blog-dev/index.php?page=privacy_policy demanderait la page de politique de confidentialité.

Maintenant que le routage a connaissance de la page demandée par l’utilisateur, il sait à quel contrôleur il doit passer le flambeau. Et cela passe bien évidemment toujours par un require().

Commençons par définir les routes de notre application dans un fichier de configuration dédié.

config/routes.php
copié !
const AVAILABLE_ROUTES = [
  'home' => 'homeController.php',
  'contact' => 'contactController.php',
  'privacy_policy' => 'privacyPolicyController.php',
  // ...
];

const DEFAULT_ROUTE = AVAILABLE_ROUTES['home'];

Maintenant, il est temps d’écrire le code du routeur qui analysera le paramètre GET page pour appeler le contrôleur adéquat.

router.php
copié !
require_once './config/routes.php';

// Récupération des clés du tableau associatif de routes
$availableRouteNames = array_keys(AVAILABLE_ROUTES);

// Check si le paramètre GET "page" existe et est bien dans le tableau de route (l'argument "true" vérifie aussi le type du paramètre)
if (isset($_GET['page']) && in_array($_GET['page'], $availableRouteNames, true)) {
	$controller = AVAILABLE_ROUTES[$_GET['page']];
} else {
	$controller = DEFAULT_ROUTE;
}

// Inclusion du contrôleur
require './controllers/' . $controller;

Mais à ce stade, notre dossier 📁 controllers n’existe pas encore…

Contrôleurs (C)

Le rôle d’un contrôleur est la plupart du temps de gérer toute la logique liée à une page avant de la retourner au client. Il peut aussi parfois s’agir de la logique associée à une action (insertion en BDD, suppression, etc.).

Dans notre MVC on va donc avoir un fichier contrôleur par page/action. Souvenez-vous, une page web peut être statique ou dynamique.

Contrôleur statique

Dans le premier cas, le travail du contrôleur sera très simple et se contentera de retourner le template HTML associé.

Ajoutons le controller associé à la page de Politique de Confidentialité de notre site :

controllers/privacyPolicyController.php
copié !
$template = './views/pages/privacy_policy.php';
Contrôleur dynamique

Dans le second cas, le contrôleur gérera toute la logique intermédiaire (récupération des variables/données) nécessaire avant de retourner le template HTML associé.

Imaginons que la page d’accueil affiche les 3 derniers articles du blog.

controllers/homeController.php
copié !
$template = './views/pages/home.php';

// Récupération des 3 derniers articles du blog en BDD via PDO et la méthode fetchAll()
$articles = ...;

Maintenant que le contrôleur sait quel template il doit inclure, il est temps de venir éditer notre 📄 index.php pour lui demander d’inclure le layout de notre page web.

index.php
copié !
require './router.php';
require './views/layout.php';

Vues (V)

Les vues sont constituées des fichiers HTML de notre projet (layout, pages et partials).

Layout

Le template global d’un site web, aussi appelé layout, est le fichier HTML représentant le squelette de notre HTML. Il contient les balises de premier niveau, communes à l’ensemble du site : <html>, <head> et <body>. À l’intérieur seront dynamiquement chargés les templates propres à chaque page (dans une balise <main> idéalement).

views/layout.php
copié !
<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <title>MVC</title>
</head>
<body>
	<main>
		<?php require($template); ?>
	</main>
</body>
</html>

Cela permet de respecter le principe du DRY en évitant de dupliquer ce squelette sur l’ensemble de nos pages. Ainsi, les templates de page ne contiendront que le code HTML spécifique à la page en question.

Pages

Les pages vont en toute logique contenir le code HTML propre à chacune des pages du site web. Ce sont elles qui vont venir s’insérer dans le <body> du fichier 📄 layout.php.

Page statique

Une page statique ne contient que du HTML statique.

views/pages/privacy_policy.php
copié !
<div class="privacy-policy">
	<h1>Politique de confidentialité</h1>
	<p>...</p>
</div>
Page dynamique

Une page dynamique manipule des données (= variables PHP) en provenance du contrôleur.

views/pages/home.php
copié !
<div class="home">
	<section id="about">
		<h1>Blog dev</h1>
		<p>...</p>
	</section>
	<section id="last-articles">
	<?php foreach ($articles as $article) { ?>
		<article>
			<h2><?= $article['title']; ?></h2>
			<a href="<?= 'index.php?page=show_article&id=' . $article['id']; ?>">Lire l'article</a>
		</article>
	<?php } ?>
	</section>
</div>

Partials

Très souvent, un site possède des « partials » (parfois aussi appelés « templates parts »). Il s’agit de portions de code qui vont être incluses dans d’autres pages afin d’améliorer la structure et la maintenabilité du site.

Il s’agit la plupart du temps du header et du footer du site.

views/partials/_header.php
copié !
<header>
	<nav>
		<a href="index.php?page=home">Accueil</a>
		...
	</nav>
</header>
views/partials/_footer.php
copié !
<footer>
	<p>Blog dev &copy; - 2024 | <a href="index.php?page=privacy_policy">Politique de confidentialité</a></p>
</footer>

Il faut donc penser à inclure ces partials dans le layout.

views/layout.php
copié !
<!DOCTYPE html>
<html lang="fr">
  <head>
    ...
  </head>
  <body>
    <?php require './views/partials/_header.php'; ?>
    ...
    <?php require './views/partials/_footer.php'; ?>
  </body>
</html>

Modèles (M)

Le modèle caractérise tout le code en charge de la manipulation des données utilisées dans un site dynamique. Ses données sont stockées dans une base de données (souvent MySQL) et les modèles vont permettre d’effectuer des opération de lecture (SELECT) et d’écriture (INSERT, UPDATE, DELETE) à son égard.

Connexion à la BDD

Créons tout d’abord un fichier de configuration dans lequel nous rassemblerons les informations de connexion à la base de données.

config/database.php
copié !
const DB_CONFIG = [
  'host'     => '127.0.0.1', // ou "localhost"
  'port'     => '3306', // ou 3307 si la connexion ne s'établit pas et que vous utilisez MariaDB
  'dbname'   => 'php_mvc_blog',
  'username' => 'root',
  'password' => '' // "root" aussi si vous utilisez MAMP
];

Ensuite, créons un fichier dédié en charge d’établir la connexion à la BDD avec PDO.

models/connection.php
copié !
require_once './config/database.php';

function dbConnect() {
	$db = new PDO(
    "mysql:host=" . DB_CONFIG['host'] . ";port=" . DB_CONFIG['port'] . ";dbname=" . DB_CONFIG['dbname'],
    DB_CONFIG['username'],
    DB_CONFIG['password']
	);
	$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
	$db->exec('SET NAMES utf8');
	return $db;
}

Gestionnaires (ou managers)

Création

Créons désormais un fichier pour chacune des tables de notre base de données. Le rôle de ces fichiers sera de contenir toutes les interactions avec la table en question de la base de données (plus généralement, les opération de type CRUD de cette dernière).

On parle de « gestionnaire » ou « manager ».

models/articleManager.php
copié !
require_once './models/connection.php';

// ===== LECTURE ===== //

// Retourne la liste des articles
function getLastArticles(int $limit): array {
  $sql = "SELECT * FROM articles ORDER BY created_at DESC LIMIT :limit";
  $query = dbConnect()->prepare($sql);
  $query->bindParam(':limit', $limit, PDO::PARAM_INT);
  $query->execute();
  $articles = $query->fetchAll();
  return $articles;
}

// Retourne un article spécifique
function getArticle(int $id): array {
  $sql = "SELECT * FROM articles WHERE id = :id";
  $query = dbConnect()->prepare($sql);
  $query->bindParam(':id', $id, PDO::PARAM_INT);
  $query->execute();
  $article = $query->fetch();
  return $article;
}

// ===== ECRITURE ===== //

// Ajoute un nouvel article en BDD
function addArticle(array $fields): void {
  $title = $fields['title'];
  $content = $fields['content'];
  $created_at = date_format(new DateTime('NOW'), 'Y-m-d H:i:s');
  $sql = "INSERT INTO articles (title, content, created_at) VALUES (:title, :content, :created_at)";
  $query = dbConnect()->prepare($sql);
  $query->bindParam(':title', $title, PDO::PARAM_STR);
  $query->bindParam(':content', $content, PDO::PARAM_STR);
  $query->bindParam(':created_at', $created_at, PDO::PARAM_STR);
  $query->execute();
}

// Supprime un article en BDD
function deleteArticle(int $id): void {
  $sql = "DELETE FROM articles WHERE id = :id";
  $query = dbConnect()->prepare($sql);
  $query->bindParam(':id', $id, PDO::PARAM_INT);
  $query->execute();
}

// Edite un article en BDD
function editArticle(int $id, array $fields): void {
  $title = $fields['title'];
  $content = $fields['content'];
  $updated_at = date_format(new DateTime('NOW'), 'Y-m-d H:i:s');
  $sql = "UPDATE articles SET title = :title, content = :content, updated_at = :updated_at WHERE id = :id";
  $query = dbConnect()->prepare($sql);
  $query->bindParam(':id', $id, PDO::PARAM_INT);
  $query->bindParam(':title', $title, PDO::PARAM_STR);
  $query->bindParam(':content', $content, PDO::PARAM_STR);
  $query->bindParam(':updated_at', $updated_at, PDO::PARAM_STR);
  $query->execute();
}
Exploitation

Ce modèle est désormais facilement exploitable depuis nos contrôleurs.

controllers/homeController.php
copié !
require_once './models/articleManager.php';

$template = './views/pages/home.php';
$articles = getLastArticles(3);
views/pages/list_articles.php
copié !
<h1>Le blog</h1>
<a href="index.php?page=add_article" role="button"><i class="fa-solid fa-plus"></i> Ajouter un article</a>
<?php foreach($articles as $article) { ?>
  <article>
    <h2><?= htmlspecialchars($article['title']) ?></h2>
    <p><?= htmlspecialchars($article['resume']) ?></p>
    <a href="index.php?page=show_article&id=<?= $article['id'] ?>" role="button" class="outline">Lire l'article</a>
  </article>
<?php } ?>

Vous avez désormais toutes les clés en main pour mettre en place une architecture MVC procédurale au sein de vos projets PHP.

Pour aller plus loin dans la compréhension et l’éllaboration d’une architecture MVC orientée objet, je vous encourage à jeter un oeil à la formation dédiée à la création de votre propre micro framework PHP.