Voir le flux RSS

Blog de CinéPhil

[Actualité] FramElem : mon framework PHP pas à pas - 3. Réécriture d'URL et routage

Noter ce billet
par , 29/06/2019 à 03h45 (2018 Affichages)
EDIT du 02/07/2019 : Renumérotation des chapitres et ajout du chapitre 4. Utilisation du routeur et test

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)
Cela veut dire que tous les fichiers programmes PHP seront des classes - même la "page.phtml" dont nous avons commencé la création dans l'article précédent et qui va donc évoluer -, mais n'allons pas trop vite...

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 un légèrement compliqué à mettre en oeuvre 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 un fichier ".htaccess" et y écrire deux règles :
Code Apache : Sélectionner tout - Visualiser dans une fenêtre à part
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&param=en
RewriteRule ^([a-zA-Z]+)\/([a-zA-Z]+)\/([a-zA-Z_]+)\/(.+)$ index.php?langue=$1&module=$2&action=$3&param=$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".
Nom : Capture_framelem_creation_classe.png
Affichages : 1740
Taille : 49,9 Ko
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 - Visualiser dans une fenêtre à part
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()
	{}
}
Vous voyez que Eclipse a automatiquement créé la classe et, puisque nous avons laissé cochées ces options dans la fenêtre de création de la classe, également l'ossature du constructeur et du destructeur.
Prenons tout de suite la bonne habitude de compléter les commentaires préformatés :
Code PHP : Sélectionner tout - Visualiser dans une fenêtre à part
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()
	{}
}
?>
Nota : Vous remarquerez que je ferme le fichier par ?>. Beaucoup ne le font pas et même certaines recommandations disent de ne pas le faire. Pourtant, en le faisant systématiquement, je n'ai à ce jour rencontré aucun problème et je trouve ça plus propre de fermer ce qui a été ouvert !

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 - Visualiser dans une fenêtre à part
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" :
Nom : Capture_framelem_creation_getters_setters.png
Affichages : 1674
Taille : 39,1 Ko
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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
<?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="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 - Visualiser dans une fenêtre à part
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à :
Nom : Capture_framelem_affichage_privoire_gabarit_page.png
Affichages : 1666
Taille : 15,3 Ko
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.

Envoyer le billet « FramElem : mon framework PHP pas à pas - 3. Réécriture d'URL et routage » dans le blog Viadeo Envoyer le billet « FramElem : mon framework PHP pas à pas - 3. Réécriture d'URL et routage » dans le blog Twitter Envoyer le billet « FramElem : mon framework PHP pas à pas - 3. Réécriture d'URL et routage » dans le blog Google Envoyer le billet « FramElem : mon framework PHP pas à pas - 3. Réécriture d'URL et routage » dans le blog Facebook Envoyer le billet « FramElem : mon framework PHP pas à pas - 3. Réécriture d'URL et routage » dans le blog Digg Envoyer le billet « FramElem : mon framework PHP pas à pas - 3. Réécriture d'URL et routage » dans le blog Delicious Envoyer le billet « FramElem : mon framework PHP pas à pas - 3. Réécriture d'URL et routage » dans le blog MySpace Envoyer le billet « FramElem : mon framework PHP pas à pas - 3. Réécriture d'URL et routage » dans le blog Yahoo

Mis à jour 22/10/2019 à 12h53 par Malick

Catégories
PHP , Développement Web , PHP

Commentaires