Aperçu des sections

  • Nous allons voir dans ce chapitre de nombreuses notions qui sont utilisés notamment dans les frameworks PHP MVC.

  • Routeur

    Un routeur est un des éléments essentiel au fonctionnement d'une application web moderne. Il permet de gérer des routes, un peu comme on aiguillerai la trajectoire d'un train sur un réseau ferré. 



    Habituellement, lorsqu'une requête arrive sur un serveur web, le serveur va appeler la page qui correspond à la requête HTTP.

    Exemples :

    • http://monsite.fr/produit.php?id=3 : exécutera le fichier produit.php en lui envoyant le paramètre id à la valeur 3
    • http://monsite.fr/nourriture/viennoiserie.php : exécutera le fichier viennoiserie.php dans le répertoire nourriture
    • etc...


    L'inconvénient de cette approche, c'est que vous allez vite vous retrouver avec de très nombreux fichiers "spécialisés", et une arborescence pleine de dossiers à ranger scrupuleusement. Cette approche devient donc vite compliquée à gérer si vous avez de nombreuses pages à traiter. Le rendu final sera une sorte de Mikado, si vous bouger un truc : tout s'effondre. De plus, cela est très rigide.


    Le fait d'utiliser un routeur vous permet d'avoir un contrôle total de la forme de vos URL, et ceci peut s'avérer très important pour des problématiques de SEO.

    Exemple sans routeur (pas bien) : /biens-immo.php?type=a&ville=toulouse&quartier=3&surface=t2&id=274

    Exemple avec routeur (bien) : /appartement/toulouse/les-minimes/t2/ref-274


    Pour ceux qui seront confrontés aux problématiques de référencement, cela est primordial. Google préférera les belles URL pleines de sens, plutôt qu'une série de paramètres qui ne veulent rien dire pour nous pauvres humains.


    Comme on peut le voir dans l'exemple ci-dessus, un routeur, c'est d'abord de la réécriture d'URL. Cela veux dire que notre lien /appartement/toulouse/les-minimes/t2/ref-274 va être transformé par le serveur web en quelque chose comme ceci : index.php/appartement/toulouse/les-minimes/t2/ref-274. Donc votre page index.php va être le point d'entrée de toutes vos requêtes HTTP.


    Pour réaliser ceci, on utilisera le fichier .htaccess avec Apache, mais tous les autres serveurs web peuvent le faire. Voici un exemple de .htaccess qui va faire de la réécriture d'URL :

    #   Active le module de réécriture d'URL.
    RewriteEngine on
    
    #   Fixe les règles de réécriture d'URL. Ici, nous utilisons une liste blanche.
    
    #   Toutes les URL qui ne correspondent pas à ces masques sont réécrites.
    RewriteCond $1 !^(index\.php)
    
    #   Toutes les autres URL vont être redirigées vers le fichier index.php.
    RewriteRule ^(.*)$ index.php/$1 [L]
    
    # IMPORTANT : exemple de réécriture d'url : /contactez-moi => /index.php/contactez-moi
    


    On pourra alors récupérer la ressource demandée via la super globale $_SERVER, et imprimer la logique que l'on souhaite. Voici un exemple de routeur très simple en PHP :

    /*Définition des routes de mon application*/
    $routes = [
    '/' => 'home.php',
    '/a-propos' => 'about.php',
    '/contactez-moi' => 'contact.php'
    ];

    /*Récupération de la route demandée dans la requête via $_SERVER*/
    $route = $_SERVER['PATH_INFO'];


    /*Affichage de la page qui correspond à la route*/
    if(array_key_exists($route, $routes)){
    require_once $routes[$route];
    } else {
    http_response_code(404); //Penser à renvoyer le bon code status HTTP (404 : ressource introuvable)
    require_once '404.php';
    }

    Pour conclure, même si la notion de routeur peut s'appliquer à plein de cas de figure, et n'est en soit pas obligatoire, elle s'utilise désormais sur toutes les applications modernes :

    • pour des problématiques de SEO (nerf de la guerre si vous devez attirer du monde sur votre site)
    • lorsque vous travaillez avec une API REST : qui trace une séparation très nette entre le front et le back. Modèle très répandu aujourd'hui sur certains type d'applications
    • s'éviter une arborescence difficile à maintenir et à comprendre


    Le routeur est donc le point d'entrée de votre application, l'aiguilleur de vos requêtes HTTP dans le back-end.

  • Le modèle MVC

    La grande majorité des frameworks PHP sont organisés autour d'une implémentation des design patterns Modèle / Vue / Contrôleur (on parle alors de MVC). Ce choix technique s'est avéré très adapté pour la réalisation d'applications web en mode client/serveur.

    Mais c'est quoi le MVC ?

    Le MVC consiste à scinder notre code en trois parties distinctes :

    1. le contrôleur : déclenché par le routeur, le contrôleur va implémenter la logique métier de votre application. Il va construire la réponse du serveur, en s'appuyant sur les données provenant du modèle, et des templates provenant des vues.
    2. le modèle : c'est la partie qui va se charger de requêter la base de données, son rôle est cantonné à ça. Le contrôleur va requêter le modèle, et le modèle aura la charge de lui retourner un jeu de données "brutes", sans mise en page ou traitements particuliers.
    3. la vue : c'est la partie qui contient les templates HTML (mais pas obligatoirement) qui serviront au contrôleur à construire la réponse finale a renvoyer au client.


    Donc pour résumer, le contrôleur et le chef d'orchestre de votre application. En fonction de la requête reçue, il va construire une réponse en s'appuyant sur les modèles pour la récupération des données, et sur les vues pour la gestion des templates.


    Voici une illustration schématique du flux d'information dans un design pattern MVC :



    Donc pour résumé : 

    1. contrôleur : réception des requêtes + logique métier + envoi des réponses
    2. modèle : échange avec la base de données (SQL)
    3. vue : stockage des templates de présentation des données destinés au client (HTML)



    Voici un exemple de requête HTTP qui permettrait d'afficher l'article 54 de votre blog favoris. Notez la présence d'un routeur :


    Voici comment on pourrait lire cette requête :

    Exécute la méthode show() de la classe Article, avec le paramètre 54

    On pourrait donc, de façon statique, le traduire comme ceci :

    require_once "articles.class.php";
    $article = new Articles();
    $article->show(54);

    Et la méthode show() s'occupe de tout : récupérer le contenu de l'article, le template HTML, et l'envoie de la réponse.


    Attention : le MVC est une sorte de "bonne pratique" institutionnalisée, mais il ne tient qu'à vous de la respecter. Ce que je sous-entends par la, c'est qu'un framework vous laisse assez de liberté pour faire n'importe quoi si vous n'avez pas compris la place que doit avoir chaque partie de votre code au sein du framework. Le framework vous offre une architecture optimisée et prête à l'emploi, à vous de l'utiliser au mieux. Comme dans tous programmes, le développeur dispose d'assez de liberté pour optimiser son code, mais aussi pour faire des bêtises... Donc à vous de bien vous imprégner de cette philosophie afin de l'appliquer correctement pour ne pas "gripper" la machine.

  • La temporisation de sortie

    Les fonctions ob_start() et ob_get_clean() sont primordiales pour la création de vues souples. L'idée, c'est qu'au lieu d'appeler simplement un fichier à l'aide d'une inclusion standard (require, include,...), on va faire en sorte de retourner le rendu de la vue sous forme de chaîne de caractère (exemple : <p>Je suis la vue accueil</p>). On pourra alors assembler chaque morceau de vues "chaines de caractères" en une seule, pour renvoyer la réponse au client au format HTML ou autre.




    Les fonctions ob_start() et ob_get_clean() permettent cela. Elles vont mettre le rendu final d'un fichier PHP non pas dans la réponse HTTP, mais dans une variable temporaire (buffer) que l'on pourra récupérer, puis traiter à souhait.


    Pour faire cela, il suffit :

    1. d'appeler la fonction ob_start() pour changer le comportement normal de PHP, et enclencher la temporisation de sortie
    2. récupérer la chaine HTML à l'aide de ob_get_clean() (ou autre)
    Cela est très utile car ça permet de construire la vue petit à petit, indépendamment du fait d'afficher réellement les templates.
    Voici la documentation officielle sur les fonctions concernant la temporisation de sortie.
  • La réponse du framework : statut, chaîne HTML

    Un des rôles du contrôleur, c'est de construire la réponse HTTP qui va être renvoyée au client (voir les images précédentes). On va donc lui rajouter certains éléments pour qu'il puisse mieux remplir ce rôle : 

    1. un attribut content : contiendra le corps de la réponse, la payload (HTML, JSON, etc...)
    2. un attribut status : contiendra le code status HTTP de la réponse (200, 404, etc...)
    3. une méthode response() : se chargera de construire la réponse finale, et de l'envoyer au client


    Cette partie se chargera notamment de modifier les différentes entêtes HTTP nécessaires dans les cas de figure plus particuliers qu'une simple page HTML.

    Par exemple :

    • chaîne JSON
    • fichier PDF à télécharger
    • etc...




    Le fait d'avoir des méthodes spécialisées dans la construction de la réponse HTTP permet d'obtenir plus de finesse dans le type de réponse que l'on souhaite retourner. C'est la tout l'intérêt de la POO et du MVC, on va mettre en place des méthodes spécialisées pour chaque besoin technique, au lieu de les laisser "se balader dans la nature" (ou plutôt, dans notre arborescence n'importe ou...).

  • Connecter le modèle à la base de données

    Maintenant que nous avons une vue et un contrôleur améliorés, nous allons intégrer PDO à notre framework, afin de pouvoir travailler avec une base de données.

    Pour ce faire, on va passer par la création d'une classe supplémentaire : Database.

    Cette classe sera spécialisée dans la connexion avec la base de données. Notre classe Model n'aura plus qu'à hériter de Database pour avoir un accès à la BDD via PDO.


    Classe Database

    La classe Database aura donc les 5 attributs suivant :

    1. hote de la db
    2. nom de la db
    3. user
    4. password
    5. pdo


    Les 4 premiers représentent les identifiants de connexion à la DB, et le 5ème est l'objet PDO retourné lors de la connexion à la DB :

    try {
    $this->pdo = new PDO("mysql:host=$this->host; dbname=$this->db;", $this->user, $this->password, $options);
    } catch (PDOException $e) {
    echo "Erreur!: " . $e->getMessage() . "<br/>";
    die();
    }



    Classe Model

    Une fois la classe Database créée, il faut que Model hérite de Database. Du coup l'objet PDO initié lors de la connexion sera accessible dans Model, on l'on pourra y exécuter des requêtes SQL :

    public function annonces(){ /* Méthode du modèle */
    $query = $this->pdo->query('SELECT * FROM annonce'); /* Hérité de Database */
    return $query->fetchAll(); /*Retourne les données au contrôleur */
    }
    Donc, toujours dans un esprit de cloisonner chaque chose à un rôle précis, on se retrouve avec la classe Database qui gère la connexion à la BDD, tandis que la classe Modèle opère les requêtes nécessaires au contrôleur.


    On récupèrera alors la liste d'annonces de cette façon depuis le contrôleur :

    $annonces = $this->model->annonces(); /*appel de la méthode annonces du modèle, depuis le controlleur */

  • Autoloading

    L'autoloading est très pratique car il vous permet de ne plus avoir à penser d'inclure les fichiers des classes avant de vous en servir. Cela se fait automatiquement, et c'est bien pour deux raisons :

    1. certaines parties de votre code peuvent avoir besoin de nombreuses classes pour fonctionner, cela évite d'avoir une "tartine" de require au début de chaque fichier
    2. cela vous enlève un travail d'ajout ou de suppression des inclusions de fichiers car le fichier ne sera appelé que si besoin (donc pas d'oubli ou d'erreurs humaines)



    Je n'ai pas trouvé mieux pour imager l'autoloading :) Mais l'idée est la... Et une petite image, ça aère le cours...


    Voici le principe de fonctionnement : quand vous souhaiter utiliser une classe (ex : $annonce = new Annonce(), ou Annonce::selectAll()), PHP "sait" que vous souhaitez charger un fichier contenant une classe, et qu'il se trouve "quelque part". On va pouvoir lui expliquer ce  "quelque part" en utilisant une fonction qui servira à charger automatiquement le bon fichier, selon la classe que vous avec appelée. La fonction spl_autoload_register() est appelée automatique lorsqu'une classe doit être utilisée.


    Si, par exemple, vous avez rangé votre classe Controller dans un dossier /core, alors vous pouvez indiquer à PHP d'inclure automatiquement le fichier /core/controller.php. Pour cela, on s'appuiera sur la fonction spl_autoload_register(), qui prends en paramètre un callback, et qui sera appelée dès qu'une classe doit être utilisée.


    Voici un exemple :



    Bien que la ligne 2 soit commentée, la ligne 7 charge bien la classe Controller située à l'emplacement définit dans la fonction spl_autoload_register() ligne 5. Le paramètre de la fonction est le nom de classe, je le force en minuscule afin que le fichier soir trouvé facilement. Dans cet exemple, les classes principales de mon framework sont bien rangées dans le dossier /core.

    Une fois ce mécanisme mis en place, vous pourrez retirer toutes vos inclusions de fichiers "manuelles".

    Voici le lien vers la documentation officielle de l'autoloading en PHP.


    Mais que faire si j'ai des classes dans d'autres dossiers ? La c'est facile, nous n'avons qu'un seul dossier contenant nos classes. Comment faire si je souhaite séparer les classes métiers des classes du framework, et créer des dossiers /controllers, /models, /libraries, etc... ?
  • Namespaces

    Les namespaces (ou espaces de nom) sont une façon de cloisonner des fichiers, et d'établir une sorte d'arborescence virtuelle au sein de votre application, décorrélée de l'arborescence de votre système de fichier. Cela existe afin de pouvoir utiliser des variables, classes ou fonctions, qui auraient potentiellement le même nom. 

    On ne souhaite pas renommer les choses par obligation, et théoriquement, on peut tout à fait se trouver dans la situation où deux classes ont le même nom. 



    Alors, on charge la classe 6 ou bien la classe 9 ?


    On va donc créer une sorte de "scope" unique à chaque fichier grâce aux namespace.

    De plus, cela va s'avérer utile pour nos autoloading. Si on doit charger des classes dans différents dossiers, comment signifier à PHP que certaines classes doivent être chargée depuis le dossier /core, et d'autres depuis le dossier /models par exemple ? Les espaces de noms permettent de faire cela.


    Il existe deux mots-clés importants pour pouvoir utiliser les namespaces :

    1. namespace : permet de déclarer le namespace (l'emplacement) d'un fichier (ex : namespace core permet de déclarer qu'un fichier appartient au namespace core)
    2. use : permet de définir de quel autre namespace on a besoin dans un fichier (ex : use core\controller permet de définir quels namespace je vais avoir besoin dans mon fichier)
    3. use ... as : permet de définir un namespace dans un format raccourci (ex : use core\controller as Controller permet d'associer l'appel de la classe Controller au namespace core\controller)


    Voici un exemple :



    Ici, pas besoin de déclarer le namespace core\controller, cela se fait automatiquement. Si une classe Controller existe dans le namespace core, alors elle répondra automatiquement au chemin core\controller.

    Les classes Vue et Model sont utilisées dans le contrôleur, on déclare les raccourci ligne 6 et 7 afin que l'autoloading fonctionne correctement ligne 22 et 23.


    Attention : les namespaces utilisent le séparateur \ au lieu de /, il faut donc le substituer dans l'autoloader afin de remplacer core\controller par core/controller.php
    Voici la documentation officielle sur les espaces de noms en PHP.
  • Les classes métiers : contrôleurs et modèles personnalisés

    Pour l'instant, notre framework ne contient que des classes "principales" : Model, Database, Vue, Controlleur, etc... En réalité, ces classes ne seront que les classes "parentes" de toutes vos classes métiers. 

    Par exemple, dans une application d'agence immobilière, vous aurez des classes Annonces, Villes, Agents, Clients, etc... et ces classes vont hériter de la classe Model. La classe modèle contiendra alors toutes les méthodes génériques, utilisables par toutes les entités, tandis que les classes "enfants" contiendront des propriétés et méthodes qui lui sont propre.

    Par exemple :

    • la classe Annonce aura les attributs prix, surface, ville_id et les méthodes activerAnnonce(), recupererPhoto(), etc...
    • la classe Client aura les attributs nom, prenom, adresse et les méthodes reserverVisite(), annonceFavorite(), etc...



    Tandis que la classe Model aura des méthodes génériques :

    • selectAll() : récupère toutes les lignes d'une table
    • select(id) : récupère une ligne d'une table via l'id
    • insert(a, b, c) : ajoute une ligne dans une table
    • etc...


    Pour les contrôleurs, on aura la même logique, mais pas d'un point de vue des données. Le contrôleur gère les requêtes HTTP, donc les différentes classes contrôleurs se feront plus par rapport a des besoins de routes. LEs cotnrolleurs répondent plus a besoin de "fonctionnalités" plutôt qu'a des problématique de stockage ou de base de donénes.

    Voici quelques exemples de contrôleurs que l'on pourrait retrouver sur un site immobilier :


    • Search : répond a tous les besoins de recherche dans le site et est appelé par différentes routes. Par exemple : /annonces/toulouse, /annonces/paris/t2, etc... ou biencore recevoir des requêtes en POST avec des critères de recherches plus précis.
    • Backoffice : gère toutes les pages du backoffice. Par exemple : /creerAnnonce, /supprimeAnnone/23, /listeCommerciaux, etc...


    De la même manière que pour le modèle, tous les contrôleurs hériteront du contrôleur principal afin de bénéficier des fonctionnalités génériques communes à tous les contrôleurs, telles que :

    • avoir accès aux modèles et aux vues
    • gérer les correctement les réponses (header HTTP, etc)
    • charger les fonctionnalités proposées par le framework (par exemple : upload de fichier ou envoi d'email, gestion de l'authentification ou des sessions)
    • etc...


    Donc la notion d'héritage va être exploitée au maximum, et on va se retrouver avec de nombreuses classes à gérer et à charger. Ce qui nous emmène aux chapitres suivant : l'autoloading et les espaces de noms.

  • extract() : ou commencer se débarrasser de $data dans les vues

    La fonction extract() est utile dans la gestion des vues. Elle permet de transformer le contenu d'un tableau en autant de variables distinctes. Cela permet de traiter les variables envoyées à la vue plus facilement.


    Exemple d'utilisation


    Cette page affichera :


    Conclusion : la vue visteExtract.php peut traiter le contenu du tableau $data avec des variables $agent, $client, $annonce, au lieu de $data['agent'], $data['client'], $data['annonce'] dans la vue visiteWithoutExtract.php. C'est donc beaucoup plus pratique et intuitif.
  • Faire un constructeur de requete SQL

    Voici un exemple de construction de requetes via des méthodes du Model (chaînées) :

    <?

    class Database {
    static public function connect(){
    try {
    return new PDO("mysql:host=localhost; dbname=agenceimmo;", 'root', '');
    } catch (PDOException $e) {
    echo "Erreur!: " . $e->getMessage() . "<br/>";
    die();
    }
    }
    }

    class Model extends Database{

    public $query = '';

    /*Constructeur de requete SQL*/
    public function select($fields){
    $this->query .= "SELECT $fields ";
    return $this;
    }

    public function from($table){
    $this->query .= "FROM $table ";
    return $this;
    }

    public function where($id){
    $this->query .= "WHERE id = $id";
    return $this;
    }



    public function exec(){
    $db = Database::connect();
    $query = $db->query($this->query);
    $query->setFetchMode(PDO::FETCH_INTO, $this);
    $query->fetch();
    return $this;
    }

    }

    class Agent extends Model {

    public $id;
    public $prenom;
    public $nom;
    public $date_embauche;
    public $created_at;
    public $updated_at;

    public function __construct(){
    echo "Agent $this->id créé !<br>";
    }

    public function formatDate(){
    $this->date_embauche = date('d-m-Y', strtotime($this->date_embauche));
    return $this; /*la méthode retourne l'objet, ce qui permet de chainer avec la méthod d'après*/
    }

    public function afficheInfo(){
    echo "<pre>";
    print_r($this);
    echo "</pre>";
    }
    }


    $agent = new Agent();
    $agent->select('*')->from('agent')->where(4)->exec()->afficheInfo();


    suite...

  • Classe Conf

    La classe Conf permet de gérer les variables d'environnements. Elle va charger le fichier env.php pour mettre a disposition toutes les variables nécessaire au fonctionnement de l'appli (identifiant DB, type d'environnement, url de l'application, etc...)

    Plus de détails dans la doc de la classe Conf.


  • Classe Router

    Afin de mieux gérer les possibilités du routeur, faire une classe Routeur qui se chargera de gérer toutes les problématiques et besoin de routing.

    Plus de détails dans la doc de la classe Router.
    Attention : le routeur décrit dans la doc détermine le contrôleur, la méthode, et les paramètres en fonction du contenu de l'URL. On en reparle.
  • Classe System

    Dans la version actuelle du framework, je crée une classe System qui gère des problématiques de haut niveau, tel que l'autoloading, la gestion de certaines erreurs, etc...

    Plus de détails dans la doc de la classe System.
  • Request

    L'objet Request représente les informations concernant la requête HTTP (information sur les entêtes "headers" et sur la charge utile "body").

    On va notamment pouvoir récupérer les input : information dans la query string (get), information dans le body (post), mais aussi des information sur l'URL par exemple (slug, fragments, etc...), ou le protocole utilisé.


    Voici la description des interfaces  PSR : https://www.php-fig.org/psr/psr-7/


    Voici un exemple d'implémentation dans Laravel : https://laravel.com/docs/8.x/requests

    On utilise souvent l'injection de dépendance pour passer un objet Request à la méthode d'un contrôleur.


    Les classes PSR sont disponibles sur github et via composer :



    Comment implémenter dans notre framework :

    • installer la lib php PSR http-message grâce à composer (dans le dossier vendor)
    • caler l'autoloading avec composer pour aller chercher les interfaces PSR dans vendor
    • créer vos propres classe Request, Message, etc... (copié/collé accepté :) )
    • implémenter les méthodes définies par les interfaces PSR
    • implémenter vos propres méthodes, votre propre logique
    • mettre en place un mécanisme d'injection de dépendances
    • tester dans l'application du framework (la todolist)

    Et voilà, on a implémenté notre propre classe  Request spécialisée, tout en respectant les standards PSR. Si les standards changent, il suffit de mettre à jour la lib http-message dans composer, puis de répercuter la nouvelle interface dans les classes de notre framework.



  • Response

    La classe Response sert à construire la réponse renvoyée au client.

    Elle agira notamment sur :

    • le code status HTTP de la réponse (404, 200, etc...)
    • le type de retour (text/html, text/json, etc...)
    • et la payload elle-même (une page HTML, une chaîne JSON, etc..)


    Exemple d'implémentation dans Laravel : https://laravel.com/docs/8.x/responses

    PSR : https://www.php-fig.org/psr/psr-7/


    Vous pouvez l'implémenter dans le framework de la même manière que Request.

  • Loggable

    Afin de pouvoir logguer différentes actions au sein du framework (requêtes SQL exécutés pendant le script, warning, erreurs, etc...) vous pouvez implémenter l'interface Loggable du PSR-3 : https://www.php-fig.org/psr/psr-3/