7 - Live Coding : PHP Orienté Objet - Le MVC

Temps de lecture : 40 minutes environ.

Nous allons parler d'un autre Design Pattern et d'une architecture qu'est le MVC.

Le MVC ? Qu'est-ce donc ?

Il s'agit d'abord d'un acronyme, qui signifie "Model View Controller", ou "Modèle Vue Contrôleur" en français.

Il s'agit surtout de la structure que nous donnerons à notre projet pour séparer clairement ses principaux composants.

En utilisant une structure MVC, nous allons séparer les accès à la base de données de notre code HTML et de toute "l'intelligence" de l'application.

Les Modèles

Les modèles seront les éléments et classes qui se chargeront de tous les échanges avec la base de données (CRUD). C'est le seul endroit de notre projet qui contiendra du SQL.

Les vues

Les vues contiendront uniquement le code HTML destiné à structurer les pages.

Les contrôleurs

Les contrôleurs, contiendront toute l'intelligence de l'application, le traitement des données en vue de leur affichage, par exemple.

Le routeur

Dans la structure MVC, un seul et unique fichier est le point d'entrée de l'application, quelle que soit la page affichée. Il est systématiquement appelé, et envoie la demande au bon contrôleur. Il est chargé de trouver le bon chemin pour que l'utilisateur récupère la bonne page, d'où le nom de routeur.

Voici un schéma qui récapitule tout ceci.

Structure de notre projet

Notre projet aura donc la structure ci-dessous

Structure du projet

Voici l'utilité des différents dossiers et fichiers

  • Controllers : contiendra, comme son nom l'indique, les contrôleurs, dont le nom commencera par une majuscule, par convention.
  • Core : ce dossier contiendra le coeur de l'application
  • Models : contiendra nos modèles, leur nom commencera également par une majuscule
  • public : Contiendra tous les fichiers accessibles au "public" par le navigateur
  • Views : contiendra nos fichiers de vues, dans des dossiers, un dossier par contrôleur.
  • Autoloader.php : notre classe de chargement automatique des fichiers

Le routeur

Le routeur est notre point d'entrée dans l'application. Nous allons le placer dans le dossier "Core" sous le nom "Main.php".

Il sera appelé par un fichier unique placé dans "public" et appelé "index.php"

Avant d'entrer dans le vif du sujet sur le contenu de ces fichiers, attardons nous sur la structure des URLs.

Pour ce premier routeur, nous allons faire simple. Nous allons mettre le nom du contrôleur et la méthode souhaitée en paramètres d'URL.

Les adresses seront donc formatées comme ceci

https://mes-annonces.test/controleur/methode

Cette structure d'URL permettra à notre routeur de savoir où diriger la demande.

Cependant, notre serveur ne contiendra pas les dossiers correspondants, il nous faudra utiliser une technique de réécriture d'URL pour parvenir à nos fins.

Le fichier .htaccess

C'est là que le fichier .htaccess entre en jeu.

Il va nous servir à réécrire notre URL à la volée pour la transformer, de façon invisible en cette url

https://mes-annonces.test/index.php?p=controleur/methode

Mais comment passer de l'une à l'autre.

En insérant ces deux lignes dans notre fichier .htaccess, que nous placerons dans "public"

RewriteEngine On
RewriteRule ^([a-zA-Z0-9\-\_\/]*)$ index.php?p=$1

Sur ces deux lignes, nous avons :

  • RewriteEngine On : permet de démarrer la réécriture d'URL
  • RewriteRule : permet de définir une règle de réécriture d'URL et fonctionne comme suit
    • ^([a-zA-Z0-9\-\_\/]*)$ : il s'agit des différents caractères pris en compte dans l'URL pour sa réécriture
      • a-z : caractères minuscules
      • A-Z : caractères majuscules
      • 0-9 : chiffres
      • \-\_\/ : tiret, underscore et / (caractère \ pour l'échappement)
      • Tout ceci entre ^( pour le début de chaîne et )$ pour la fin de chaîne
    • index.php?p=$1 : $1 contiendra le résultat de la réécriture notre chaîne

Le fichier index.php

Dans notre fichier index.php, nous allons uniquement charger l'autoloader (vu dans la partie autoload) et instancier notre classe "Main" puis démarrer l'application.

// On définit une constante contenant le dossier racine
define('ROOT', dirname(__DIR__));

// On importe les namespaces nécessaires
use App\Autoloader;
use App\Core\Main;

// On importe l'Autoloader
require_once ROOT.'/Autoloader.php';
Autoloader::register();

// On instancie Main
$app = new Main();

// On démarre l'application
$app->start();

Le coeur de l'application (Core)

Les fichiers correspondant au coeur de l'application seront placés dans le dossier "Core"

La classe Main

Véritable coeur de l'application, la classe Main nous permet de router les demandes vers les différents contrôleurs.

Nous devons récupérer les paramètres d'URL (p=controleur/méthode), vérifier si ils existent et enfin charger le contrôleur et la méthode concernés.

Tout d'abord, nous créons la classe, son namespace et la méthode "start" utilisée dans index.php

namespace App\Core;

class Main
{
public function start()
{

}
}

Nous allons ensuite tout faire dans cette méthode "start".

Nous commencerons par retirer l'éventuel "trailing slash" (/ à la fin de l'url) pour simplifier le traitement des paramètres.

En effet, "/controleur/methode/" donnera 3 paramètres où "/controleur/methode" en donnera 2, et ça nous permet également d'éviter le "Duplicate Content"

// On récupère l'adresse
$uri = $_SERVER['REQUEST_URI'];

// On vérifie si elle n'est pas vide et si elle se termine par un /
if(!empty($uri) && $uri != '/' && $uri[-1] === '/'){
// Dans ce cas on enlève le /
$uri = substr($uri, 0, -1);

// On envoie une redirection permanente
http_response_code(301);

// On redirige vers l'URL dans /
header('Location: '.$uri);
exit;
}

Nous allons ensuite séparer les paramètres de l'URL dans un tableau pour effectuer le traitement, vérifier si ils existent et instancier le bon contrôleur puis appeler la bonne méthode.

// On sépare les paramètres et on les met dans le tableau $params
$params = explode('/', $_GET['p']);

// Si au moins 1 paramètre existe
if($params[0] != ""){
// On sauvegarde le 1er paramètre dans $controller en mettant sa 1ère lettre en majuscule, en ajoutant le namespace des controleurs et en ajoutant "Controller" à la fin
$controller = '\\App\\Controllers\\'.ucfirst(array_shift($params)).'Controller';

// On sauvegarde le 2ème paramètre dans $action si il existe, sinon index
$action = isset($params[0]) ? array_shift($params) : 'index';

// On instancie le contrôleur
$controller = new $controller();

if(method_exists($controller, $action)){
// Si il reste des paramètres, on appelle la méthode en envoyant les paramètres sinon on l'appelle "à vide"
(isset($params[0])) ? $controller->$action($params) : $controller->$action();
}else{
// On envoie le code réponse 404
http_response_code(404);
echo "La page recherchée n'existe pas";
}
}else{
// Ici aucun paramètre n'est défini
// On instancie le contrôleur par défaut (page d'accueil)
$controller = new Controllers\MainController();

// On appelle la méthode index
$controller->index();
}

Voilà notre routeur prêt à fonctionner. Il faudra, bien sûr, ajouter des contrôles d'erreur et des remontées de pages 404, par exemple.

La classe Db

Vue dans la partie sur la base de données, notre classe Db permettra de nous connecter à la base de données. Nous allons reprendre le même code et modifier uniquement le namespace.

namespace App\Core;

// On "importe" PDO
use PDO;
use PDOException;

class Db extends PDO
{
// Instance unique de la classe
private static $instance;

// Informations de connexion
private const DBHOST = 'localhost';
private const DBUSER = 'root';
private const DBPASS = '';
private const DBNAME = 'demo_poo';

private function __construct()
{
// DSN de connexion
$_dsn = 'mysql:dbname='. self::DBNAME . ';host=' . self::DBHOST;

// On appelle le constructeur de la classe PDO
try{
parent::__construct($_dsn, self::DBUSER, self::DBPASS);

$this->setAttribute(PDO::MYSQL_ATTR_INIT_COMMAND, 'SET NAMES utf8');
$this->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}catch(PDOException $e){
die($e->getMessage());
}
}


public static function getInstance():self
{
if(self::$instance === null){
self::$instance = new self();
}
return self::$instance;
}
}

Les contrôleurs

Ce sont les tours de contrôle de notre application web, les contrôleurs vont contenir tout le code de traitement de nos pages. Ils seront séparés en fonction du type de contenu qu'ils auront à gérer, par exemple "AnnoncesController" pour les annonces, "UsersController" pour les utilisateurs...

Le contrôleur principal

Le contrôleur principal sera une classe abstraite qui contiendra les méthodes communes à tous les contrôleurs de notre application. Nous y reviendrons à plusieurs reprises pour y ajouter de nouvelles méthodes.

Pour le moment, nous allons uniquement y écrire son namespace et sa classe. Créons le fichier "Controller.php" dans "Controllers"

namespace App\Controllers;

abstract class Controller{

}

Le contrôleur par défaut

Nous avons besoin d'un contrôleur par défaut pour notre page d'accueil. Pour la gérer, nous utiliserons le "MainController" que nous placerons dans "Controllers". Pour le moment, nous afficherons uniquement "Vous êtes sur la page d'accueil"

namespace App\Controllers;

class MainController extends Controller
{
public function index()
{
echo 'Vous êtes sur la page d\'accueil';
}
}

Le contrôleur "Annonces"

Pour afficher nos annonces, nous allons créer un contrôleur spécifique qui permettra de gérer les différents cas de figure (une annonce, une catégorie...). Nous l'appellerons "AnnoncesController" et nous le placerons dans le dossier "Controllers".

Pour le moment, nous n'allons rien ajouter mis à part une méthode "index" qui affichera un texte.

namespace App\Controllers;

class AnnoncesController extends Controller
{
public function index(){
echo 'Ici sera la liste des annonces';
}
}

Les modèles

Les modèles sont les fichiers du MVC qui nous servent à intéragir avec la base de données. L'un d'entre eux, le modèle principal, traité dans la partie précédente, permet d'exécuter les requêtes les plus courantes.

Le modèle principal

Comme indiqué, ce fichier "Model.php" est la pierre angulaire de la gestion de la base de données. Il permet de mettre en place les requêtes principales qui seront utilisées par les "sous modèles"

Nous reprenons le fichier de la partie "Base de données" en modifiant son namespace et l'importation de la classe Db

<?php
namespace App\Models;

use App\Core\Db;

class Model extends Db
{
// Table de la base de données
protected $table;

// Instance de Db
private $db;


public function findAll()
{
$query = $this->requete('SELECT * FROM '. $this->table);
return $query->fetchAll();
}

public function findBy(array $criteres)
{
$champs = [];
$valeurs = [];

// On boucle pour éclater le tableau
foreach($criteres as $champ => $valeur){
// SELECT * FROM annonces WHERE actif = ? AND signale = 0
// bindValue(1, valeur)
$champs[] = "$champ = ?";
$valeurs[] = $valeur;
}

// On transforme le tableau "champs" en une chaine de caractères
$liste_champs = implode(' AND ', $champs);

// On exécute la requête
return $this->requete('SELECT * FROM '.$this->table.' WHERE '. $liste_champs, $valeurs)->fetchAll();
}

public function find(int $id)
{
return $this->requete("SELECT * FROM {$this->table} WHERE id = $id")->fetch();
}

public function create(Model $model)
{
$champs = [];
$inter = [];
$valeurs = [];

// On boucle pour éclater le tableau
foreach($model as $champ => $valeur){
// INSERT INTO annonces (titre, description, actif) VALUES (?, ?, ?)
if($valeur != null && $champ != 'db' && $champ != 'table'){
$champs[] = $champ;
$inter[] = "?";
$valeurs[] = $valeur;
}
}

// On transforme le tableau "champs" en une chaine de caractères
$liste_champs = implode(', ', $champs);
$liste_inter = implode(', ', $inter);

// On exécute la requête
return $this->requete('INSERT INTO '.$this->table.' ('. $liste_champs.')VALUES('.$liste_inter.')', $valeurs);
}

public function update(int $id, Model $model)
{
$champs = [];
$valeurs = [];

// On boucle pour éclater le tableau
foreach($model as $champ => $valeur){
// UPDATE annonces SET titre = ?, description = ?, actif = ? WHERE id= ?
if($valeur !== null && $champ != 'db' && $champ != 'table'){
$champs[] = "$champ = ?";
$valeurs[] = $valeur;
}
}
$valeurs[] = $id;

// On transforme le tableau "champs" en une chaine de caractères
$liste_champs = implode(', ', $champs);

// On exécute la requête
return $this->requete('UPDATE '.$this->table.' SET '. $liste_champs.' WHERE id = ?', $valeurs);
}

public function delete(int $id)
{
return $this->requete("DELETE FROM {$this->table} WHERE id = ?", [$id]);
}


public function requete(string $sql, array $attributs = null)
{
// On récupère l'instance de Db
$this->db = Db::getInstance();

// On vérifie si on a des attributs
if($attributs !== null){
// Requête préparée
$query = $this->db->prepare($sql);
$query->execute($attributs);
return $query;
}else{
// Requête simple
return $this->db->query($sql);
}
}


public function hydrate(array $donnees)
{
foreach($donnees as $key => $value){
// On récupère le nom du setter correspondant à la clé (key)
// titre -> setTitre
$setter = 'set'.ucfirst($key);

// On vérifie si le setter existe
if(method_exists($this, $setter)){
// On appelle le setter
$this->$setter($value);
}
}
return $this;
}
}

Le modèle "Annonces"

Nous allons également reprendre le modèle appelé "AnnoncesModel" qui permet de gérer la table "annonces"

namespace App\Models;

class AnnoncesModel extends Model
{
protected $id;
protected $titre;
protected $description;
protected $created_at;
protected $actif;

public function __construct()
{
$this->table = 'annonces';
}

/**
* Get the value of id
*/
public function getId()
{
return $this->id;
}

/**
* Set the value of id
*
* @return self
*/
public function setId($id)
{
$this->id = $id;

return $this;
}

/**
* Get the value of titre
*/
public function getTitre()
{
return $this->titre;
}

/**
* Set the value of titre
*
* @return self
*/
public function setTitre($titre)
{
$this->titre = $titre;

return $this;
}

/**
* Get the value of description
*/
public function getDescription()
{
return $this->description;
}

/**
* Set the value of description
*
* @return self
*/
public function setDescription($description)
{
$this->description = $description;

return $this;
}

/**
* Get the value of created_at
*/
public function getCreated_at()
{
return $this->created_at;
}

/**
* Set the value of created_at
*
* @return self
*/
public function setCreated_at($created_at)
{
$this->created_at = $created_at;

return $this;
}

/**
* Get the value of actif
*/
public function getActif()
{
return $this->actif;
}

/**
* Set the value of actif
*
* @return self
*/
public function setActif($actif)
{
$this->actif = $actif;

return $this;
}
}

Les vues

Dans le dossier "Views", nous appliquerons une convention qui nous permettra d'identifier rapidement le lien entre le contrôleur et la vue.

En effet, nous créerons un dossier pour chaque contrôleur puis à l'intérieur un fichier pour chaque méthode.

Nous aurons donc, par exemple, un dossier "annonces" pour le contrôleur "AnnoncesController" et un fichier "index.php" pour la méthode "index".

Obtenir de l'aide

Pour obtenir de l'aide, vous pouvez accéder au serveur Discord pour une entraide par chat

7 - Live Coding : PHP Orienté Objet - Le MVC
Article publié le - Modifié le

Catégories : MVC PHP Tutoriel Live-Coding poo

Mots-clés : Tutoriel MVC PHP Live-Coding POO Orienté Objet objet

Partager : Partager sur Facebook Partager sur Twitter Partager sur LinkedIn