Live Coding : PHP Orienté Objet - Le MVC

Par Nouvelle-Techno.fr le 28 mai 2020 - Catégories : MVC PHP Tutoriel Live-Coding

Lire l'article sur le site d'origine

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

Voici l'utilité des différents dossiers et 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 :

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

#Tutoriel #MVC #PHP #Live-Coding #Orienté Objet