Avant propos
Nous allons maintenant mettre les mains dans le cambouis et construire le "moteur" du framework et de notre futur site web !
Mais avant ça, je reviens sur une précision donnée dans le premier article sur les principes :
Le développement sera fait en programmation orientée objet (POO) selon l'architecture Modèle, Vue, Contrôleur (MVC)
1. Principes de fonctionnement du moteur de l'application
Le "moteur" du framework se composera :
- d'un mécanisme de traduction de l'URL envoyée par le poste de l'utilisateur pour que nos programmes les comprennent ;
- d'un mécanisme de routage pour appeler le bon programme en fonction de l'URL demandée ;
- d'un contrôle de validité de l'URL demandée pour combattre les éventuels piratages et autres malveillances.
2. Réécriture d'URL (ou URL Rewriting)
Sous cette appellation se cache un principe simple, mais légèrement compliqué à mettre en œuvre et à programmer quand on n'est pas habitué aux expressions régulières. Il s'agit de faire en sorte que les URL soient en langage assez clair pour un humain (ou pour un moteur de recherche qui va plus facilement référencer votre site ) et qu'une mécanique interne au site comprenne quel programme appeler avec quels éventuels paramètres pour retourner à l'utilisateur ce qu'il a demandé. Je vous renvoie à la FAQ pour plus de détails sur ce qu'il faut faire côté serveur Apache.
Les URL du site auront le style suivant : monsite.com/langue/module/action/parametres.
Par exemple, avec l'URL : monsite.com/fr/accueil/changerLangue/en => on comprend facilement que le site est actuellement en français (fr), que nous sommes dans le module "accueil", que l'action que nous souhaitons faire est de changer de langue avec comme paramètre la langue choisie : l'anglais (en).
Je ne vous redonne pas la méthode de création d'un fichier dans Eclipse (voir chapitre 2 du précédent article).
Nous allons créer à la racine du site, c'est à dire dans le dossier "public", un fichier ".htaccess" et y écrire deux règles :
Code Apache : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # .htaccess # Accès contrôlé au site et réécriture d'URL # # @author Philippe Leménager # @version V0.1 - plemenager - 2019-06-25 - Création # # Historique : # # # Réécriture d'URL # Exemple : lesite.com/fr/Accueil/changerLangue/en => index.php?langue=fr&module=Accueil&action=changerLangue¶m=en RewriteRule ^([a-zA-Z]+)\/([a-zA-Z]+)\/([a-zA-Z_]+)\/(.+)$ index.php?langue=$1&module=$2&action=$3¶m=$4 [L] # Règle idem sans le paramètre final RewriteRule ^([a-zA-Z]+)\/([a-zA-Z]+)\/([a-zA-Z_]+)$ index.php?langue=$1&module=$2&action=$3 [L] |
La première règle du code ci-dessus va donc découper l'URL en 4 morceaux pour dire à php que le premier morceau sera la langue, le deuxième le module...
La seconde règle est là pour tenir compte des actions qui ne comportent pas de paramètre et qui n'ont donc que trois morceaux à traduire en variable php.
Finalement, c'est simple, non ?
3. Le routeur
Nous allons maintenant écrire notre première classe PHP : "Routeur".
Le routeur est chargé de décortiquer l'URL récrite par le .htaccess pour savoir quel contrôleur lancer et lui passer les paramètres éventuels nécessaires pour donner la bonne réponse à ce qu'a demandé l'utilisateur du site.
Commençons par créer avec Eclipse la classe "Routeur" :
1. Si ce n'est déjà fait, créer le dossier "framelem/application/controllers" (voir la méthode au chapitre 1 de l'article précédent), car le routeur est l'un des principaux contrôleurs de l'application PHP que nous sommes en train de construire.
2. Créons maintenant la classe à l'intérieur de ce dossier.
Faire un clic droit sur le dossier, puis choisir "New / Class".
Saisir le nom de la classe "Routeur" dans la zone "Class Name" puis cliquer sur "Finish". Eclipse crée automatiquement le fichier "Routeur.php" (et il est important que la racine du nom du fichier soit le nom de la classe ! ) et l'ouvre. Ça ressemble à ceci :
Code PHP : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php namespace application\controllers; /** * * @author philippe * */ class Routeur { /** */ public function __construct() {} /** */ function __destruct() {} } |
Prenons tout de suite la bonne habitude de compléter les commentaires préformatés :
Code PHP : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <?php namespace application\controllers; /** * Routeur des requêtes http vers le bon contrôleur à exécuter. * * @filesource application/controllers/ * @author Philippe Leménager * @version V0.1 - plemenager - 2019-06-26 - Création */ class Routeur { /** * Constructeur */ public function __construct() {} /** * Destructeur */ function __destruct() {} } ?> |
Et si nous tapions enfin nos premières lignes de code ?
Comme dit précédemment, le mécanisme de réécriture d'URL va nous livrer les données "langue", "module", "action" et, éventuellement, "parametre". Tout ceci va se trouver dans le tableau PHP $_REQUEST. Notre classe routeur aura donc comme premiers attributs ces éléments. Déclarons-les en tête de classe (je ne donne ci-dessous que la partie de code concerné) :
Code PHP : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class Routeur { /** * Langue applicable * @var string */ private $langue; /** * Module où se trouve l'action à lancer * @var string */ private $module; /** * Action à lancer * @var string */ private $action; /** * Paramètre(s) éventuel(s) * @var string */ private $parametres; /** * Constructeur */ public function __construct() {} |
Créons maintenant tout de suite les getters et setters de ces attributs. Faire un clic droit n'importe où dans le code puis choisir "Source / Generate Getters And Setters". Cliquer sur "Select All". Dans "Insertion point", ma préférence va à "After '__destruct()'" et dans "Sort By" à "Fields in getter/setter pairs". Enfin, cliquer sur "OK" :
Aérons le code en passant une ligne après la méthode __destruct() puis complétons les commentaires et ces méthodes nouvelles :
- Ici, les setters restent internes au routeur et ne doivent pas être accessibles de l'extérieur de la classe => on passe donc les setters en private
- Les valeurs données aux setters doivent être contrôlées pour ne pas donner de résultats incohérents.
Voilà ce que ça donne :
Code PHP : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | /** * Destructeur */ function __destruct() {} /** * Retourne le code de la langue à utiliser. * @return string */ public function getLangue() { return $this->langue; } /** * Affecte le code de la langue à utiliser au routeur * @param string $langue */ public function setLangue($langue) { $this->langue = $langue; } /** * Retourne le nom du module * @return string */ public function getModule() { return $this->module; } /** * Affecte le module au routeur * @access private * @param string $module */ private function setModule($module = 'Accueil') { $this->module = $module; } /** * Retourne l'action demandée * @return string */ public function getAction() { return $this->action; } /** * Affecte l'action demandée au routeur * @access private * @param string $action */ private function setAction($action) { $this->action = $action; } /** * Retourne la liste des paramètres éventuels donnés par l'URI * @return string */ public function getParametres() { return $this->parametres; } /** * Affecte les paramètres éventuels de l'URI au routeur * @access private * @param string $parametres */ private function setParametres($parametres) { $this->parametres = $parametres; } |
Créons maintenant après ces getters et setters une méthode appelerAction() qui, comme son nom l'indique, va lancer le contrôleur de l'action à lancer dans le bon module :
Code PHP : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /** * Instancie le contrôleur requis et lance l'action demandée */ public function appelerAction() { // Construction du chemin vers la classe de l'action $classeControleur = 'application\\modules\\'.$this->getModule().'\\controllers\\'.$this->getAction(); // Instanciation de la classe contrôleur de l'action $controleur = new $classeControleur(); // Appel de la méthode par défaut de toute classe d'action $controleur->index(); } |
Les attributs de la classe seront valorisés par le constructeur du Routeur, à partir de ce qui est fourni dans la variable globale $_REQUEST :
Code PHP : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | /** * Constructeur */ private function __construct() { // Décomposition de l'URI renvoyée par .htaccess if(empty($_REQUEST)) { // Si pas de REQUEST => accès direct à la page d'accueil du site $this->setLangue('fr'); // Langue par défaut = français $this->setModule('Accueil'); $this->setAction('Accueil'); } else { // Une REQUEST a été demandée $this->setLangue($_REQUEST['langue']); $this->setModule($_REQUEST['module']); $this->setAction($_REQUEST['action']); if(isset($_REQUEST['param'])) { $this->setParametres($_REQUEST['param']); } } } // Fin function __construct() |
Les informations que contient le routeur pourront être utiles à toutes les classes lancées suite à une requête HTTP. J'ai donc choisi de faire de ce routeur un singleton afin que son instance soit accessible de tous les programmes.
Pour faire un singleton, il faut :
- rendre le constructeur private afin que la classe ne puisse être instanciée que par elle-même ;
- créer un attribut statique porteur de l'instance de la classe ;
- créer une méthode statique qui appelle l'instance ou l'initialise si ça n'a pas déjà été fait auparavant.
Voilà ce que ça donne (extraits utiles à ce point) :
Code PHP : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | class Routeur { /** * Instance du singleton Routeur * @staticvar Routeur */ private static $instance = null; // Déclaration des autres attributs /** * Constructeur lancé par getInstance() * @access private */ private function __construct() { // ... } // Fin private function __construct() /** * Destructeur */ function __destruct() {} /** * Permet l'instanciation du singleton Routeur * @return Routeur */ public static function getInstance() { if(is_null(self::$instance)) { self::$instance = new self; } return self::$instance; } |
4. Utilisation du routeur et test
Vous êtes sans doute impatient de commencer à afficher des choses, non ? Alors voyons comment le routeur fonctionne et testons-le...
Commençons par programmer le lancement du routeur dans le fichier d'entrée universel sur le site : "framelem/index.php".
J'ajoute provisoirement en tête de fichier les instructions nécessaires à l'affichage des erreurs PHP. Une fois le fichier index.php testé et validé, l'affichage des erreurs sera programmé ailleurs seulement si le site n'est pas en production.
En fin de fichier, on ajoute le lancement effectif du routeur. Voici le code complet de cette version 0.2 de index.php :
Code PHP : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | <?php /** * Point d'accès unique dans le site. * * Traite l'URL appelée par l'utilisateur et oriente vers le contrôleur correspondant * * @filesource index.php * @author Philippe Leménager * @version V0.2 - 2019-07-02 - plemenager - Ajout du lancement du routeur et affichage provisoire des erreurs PHP. */ /* Historique : * V0.1 - 2019-06-20 - plemenager - Création */ // Affichage des erreurs // TODO à supprimer en prod ini_set('display_errors', 1); error_reporting(E_ALL); // Ajout d'un slash final au répertoire racine du site define('DIR_ROOT', __DIR__.DIRECTORY_SEPARATOR); /** * Autoloader des classes en suivant les namespaces. * * Code fourni par rawsrc (https://www.developpez.net/forums/blogs/32058-rawsrc/b5109/autoloader/) * * @param string $full_class_name : Nom complet de la classe (avec son espace de nom) */ $autoloader = function($full_class_name) { // on prépare le terrain : on remplace le séparateur d'espace de nom par le séparateur de répertoires du système $name = str_replace('\\', DIRECTORY_SEPARATOR, $full_class_name); // on construit le chemin complet du fichier à inclure : // il faut que l'autoloader soit toujours à la racine du site : tout part de là avec __DIR__ $path = DIR_ROOT.$name.'.php'; // on vérfie que le fichier existe et on l'inclut // sinon on passe la main à un autre autoloader (return false) if (is_file($path)) { include $path; } else { return false; } }; // On enregistre la fonction dans le registre d'autoload spl_autoload_register($autoloader); /** * Lancement du routeur et appel de l'action demandée ou par défaut */ use application\controllers\Routeur; Routeur::getInstance()->appelerAction(); ?> |
Vous voyez qu'on commence par charger le routeur en mémoire grâce à la fonction $autoloader puis on instancie la classe Routeur et on appelle sa fonction appelerAction().
Puisque, dans le routeur, le module et l'action par défaut s'appellent "Accueil", créons, si ce n'est déjà fait, l'arborescence de dossiers "framelem/application/modules/Accueil/controllers/" puis créons-y la classe "Accueil" :
Code PHP : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php namespace application\modules\Accueil\controllers; /** * Classe contrôleur de l'accueil du site * * @filesource application/modules/Accueil/controllers/Accueil.php * @author Philippe Leménager * @version V0.1 - plemenager - 02/07/2019 - Création */ class Accueil { /** */ public function __construct() {} /** */ function __destruct() {} } ?> |
Ajoutons la méthode "index()" qui va, pour ce moment de test, simplement appeler la vue "application/views/page.phtml" :
Code PHP : | Sélectionner tout |
1 2 3 4 5 | public function index() { // FIXME Affichage provisoire du masque des pages du site (à modifier plus tard) require DIR_ROOT.'application/views/page.phtml'; } |
Allons maintenant faire afficher quelque chose dans "page.phtml". Voici le nouveau code complet de la version 0.2 :
Code HTML : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | <?php /** * Gabarit général des pages web. * * @filesource page.phtml * @author Philippe Leménager * @version V0.2 - 2019-07-02 - plemenager - Affichage d'un texte provisoire pour tests */ /* Historique : * @version V0.1 - 21 juin 2019 - plemenager - Création */ ?> <!DOCTYPE html> <html> <head> <title>FramElem - Accueil</title> <meta charset="utf-8" /> <meta name="description" lang="fr" content="FramElem - Un framework PHP élémentaire" /> <meta name="keywords" lang="fr" content="FramElem, framework, PHP" /> <meta name="author" lang="fr" content="Philippe LEMÉNAGER" /> <meta name="viewport" content="width=device-width"> <link rel="stylesheet" href="Public/css/general.css" type="text/css"> </head> <body> <div id="header"> <!-- Contient l'en-tête de la page --> </div> <div id="content"> <!-- Contient les informations publiées par le site --> <h1>FramElem : un framework PHP élémentaire !</h1> <p>Ceci est un affichage provisoire à fins de tests.</p> </div> <div id="footer"> <!-- Contient le pied de page !--> </div> </body> </html> |
Pour voir ce que ça donne, il faut rendre le site accessible. Sur mon ordinateur sous Mageia Linux, la racine des sites web est "/var/www/html", mais le dossier du projet est, lui, dans "/home/philippe/eclipse-workspace/framelem". Je passe donc en mode console sous l'utilisateur administrateur nommé "root" et j'ajoute dans "/var/www/html" un lien symbolique "framelem" qui pointe vers "/home/philippe/eclipse-workspace/framelem" :
Code bash : | Sélectionner tout |
1 2 | [root@localhost ~]# cd /var/www/html [root@localhost html]# ln -s /home/philippe/eclipse-workspace/framelem framelem |
Il suffit maintenant que je tape "localhost/framelem" dans mon navigateur... et voilà :
Ce n'est pas sexy, mais ça fonctionne !
Ce routeur est donc fonctionnel... mais incomplet, car il doit aussi vérifier que la langue, le module et l'action demandés existent dans l'application. C'est ce que nous ferons dans le prochain article.