Framy - Partie 4 - Url-rewritons !!
Par Wam mania le lundi, août 25 2008, 20:24 - Framy - Lien permanent
Cet article fait suite à la partie 3
Nous voici donc arrivé à cette grosse partie.
Je vais commencer par expliquer ce qu'on cherche à faire.
Une application est constituée de un ou plusieurs contrôleurs, contenant des actions étroitement liées au contrôleur.
Nous allons créer un contrôleur application, contenant 1 action : helloworld qui prendra en paramètre la langue (en ou fr), ainsi que la période de la journée (matin, aprem, soir, nuit)
Nous devons maintenant appeler ce couple contrôleur/action.
Pour les pressés, voici le code complet de notre Url Rewriting :
Télécharger le code complet
Commençons par étudier les 3 types d'URL possibles :
- Les urls basiques. On appelle une page physique, puis on place les paramètres derrière un ?
Par exemple : http://www.tonsite.com/index.php?controller=application&action=helloworld&lang=fr&periode=soir L'avantage c'est que c'est simple et que ça fonctionne partout, mais c'est moche et google déteste.
- L'utilisation de l'option multiviews d'apache et du path_info de PHP.
Ceci permet d'avoir des url du style : http://www.tonsite.com/index/application/helloworld/fr/soir
Ici, lorsqu'on utilise /index/ , apache cherche en 1er le répertoire index, s'il est absent, il cherchera un fichier index.*
L'avantage c'est que les urls sont propres, mais les inconvénients sont à mon avis bien plus importants :
- Il ne doit pas y avoir de répertoire (ou de fichier du même nom avec une extension différente et prioritaire, comme .html) portant le nom du fichier php qu'on appelle.
- L'option multiviews d'apache n'est pas forcement activée partout
- Le module mod_rewrite d'apache
Il est nécessaire d'avoir le module activé sur apache et d'avoir le droit de placer un .htaccess dans la racine du site. On aura des URL du type : http://www.tonsite.com/application/helloworld/fr/soir Le mod_rewrite est la meilleur solution pour obtenir des urls propres.
Nous verrons plus tard qu'avec le multiviews, et surtout avec le mod_rewrite, on peut créer des URLs bien plus personnalisables que les exemples cités.
Nous venons de voir l'aspect théorique, "ce qu'on peut faire avec les différents mods". Voyons maintenant ce que nous voulons, ça nous permettra de rassembler les traitements communs et aussi, de séparer certains traitements en 2.
En effet, malgré la forme de l'urls qui diffère, les informations qui sont transmises au final ne prennent que deux formes. Je m'explique par un exemple concret :
- multiviews : http://www.wamania.com/index/application/helloworld/fr/soir
- Mod_rewrite : http://www.wamania.com/application/helloworld/fr/soir
- Urls "basique" : http://www.wamania.com/index.php?q=/application/helloworld/fr/soir (que nous appelerons le mod "Query_String")
- ou plus simplement : http://www.wamania.com/index.php?controller=application&action=helloworld&lang=fr&perdiode=soir (que nous appellerons simplement "simple")
On voit donc que dans les 3 premiers, seul la manière de transmettre la chaîne diffère, mais le traitement de la chaîne sera semblable. Seul le dernier cas est un cas particulier. Nous nommerons cette chaîne ( /application/helloworld/fr/soir ) la "Param_String".
Cette chaîne param_string est ce qui nous intéresse vraiment. En effet, même si nous pouvons l'utiliser comme ici /controller/action/params1/params2 , nous pouvons aussi être beaucoup plus créatif. Par exemple, dans notre helloworld en français et le soir, quel message voulons nous passer ? Que voulons nous que google retienne ? Le contenu ne sera pas "Hello World", mais "Bonsoir le monde". Pourquoi donc ne pas avoir une url qui concorde ?
Par exemple, implicitement la param_string /bonsoir-le-monde nous dis : c'est le soir, c'est en français. Nos 2 paramètres sont là, il ne manque plus qu'à lier cette param_string au couple controller/action application/helloworld.
Pas de soucis, nous pouvons le faire !!
Pour cela, nous utiliserons un fichier routes.php que nous verrons à la fin.
Nous allons donc avoir les fichiers
- index.php
- framy/boot.php
- framy/request.php
- framy/url/urlengine.interface.php
- framy/url/simple.urlengine.php
- framy/url/multiviews.urlengine.php
- framy/url/mod_rewrite.urlengine.php
- framy/url/query_string.urlengine.php
- framy/url/param_string.php
- app/applicationController.php
- app/routes.php
Ainsi qu'un fichier .htaccess

Commençons par le fichier index.php, c'est le plus simple, il se contente d'inclure les autres fichiers, puis d'appeler la méthode static launch de la classe boot :
<?php
require_once './framy/boot.php';
require_once './framy/request.php';
require_once './framy/url/urlengine.interface.php';
require_once './framy/url/param_string.php';
require_once './framy/url/multiviews.urlengine.php';
require_once './framy/url/query_string.urlengine.php';
require_once './framy/url/mod_rewrite.urlengine.php';
require_once './framy/url/simple.urlengine.php';
//define('URL_HANDLER', 'simple');
//define('URL_HANDLER', 'querystring');
//define('URL_HANDLER', 'multiviews');
define('URL_HANDLER', 'modrewrite');
yBoot::launch();
?>
Ensuite le fichier framy/boot.php
<?php
/**
* Classe yBoot
* @return void
*/
class yBoot {
/**
* Méthode launch, lancement du programme
* @return void
*/
public static function launch() {
// On crée l'objet de yRequest qui supportera
// les éléments de la requète de l'utilisateur
$request = new yRequest();
// On récupère ce paramètre,
// notamment le couple contrôleur/action
$params = $request->getParams();
require_once './app/'.$params['controller'].'Controller.php';
$controller = $params['controller'].'Controller';
$oController = new $controller ($params);
$oController->{$params['action']} ();
}
}
?>
Notre classe qui contient la requête et qui lance le traitement de l'URL. Nous ajoutons un petit traitement "par défaut" exceptionnel pour diriger les pages sans controller/action vers application/helloworld
<?php
class yRequest {
private $params;
public function init() {
$param_string = '';
// Récupération de notre "param_string" que nous
// abrégeons dans la requête par "ps"
switch(URL_HANDLER) {
case 'simple' :
break;
case 'querystring' :
$param_string = isset($_GET['ps']) ? $_GET['ps'] : '';
// On enleve le premier / car pour le mod_rewrite, il n'y est pas
$param_string = substr($param_string, 1, strlen($param_string)-1);
break;
case 'multiviews' :
$param_string = substr($_SERVER['PATH_INFO'], 1, strlen($_SERVER['PATH_INFO'])-1);
break;
case 'modrewrite' :
$param_string = isset($_GET['ps']) ? $_GET['ps'] : '';
break;
default :
throw new Exception ("Impossible de trouver un support pour ce type d'url");
break;
}
$urlEngineClassName = 'y'.ucwords(URL_HANDLER).'Url';
try {
$urlEngine = new $urlEngineClassName;
$this->params = $urlEngine->url2params($param_string, $_GET);
} catch (Exception $e) {
echo $e->getMessage();die();
}
// Juste pour notre exemple, si on a rien mis, on redirige vers application/helloworld
if (empty($this->params['controller'])) { $this->params['controller'] = 'application';}
if (empty($this->params['action'])) { $this->params['action'] = 'helloworld';}
if (empty($this->params['lang'])) { $this->params['lang'] = 'fr'; }
if (empty($this->params['periode'])) { $this->params['periode'] = 'soir'; }
$this->params = array_merge($this->params, $_POST);
}
public function getParams() {
if (empty($this->params)) {
$this->init();
}
return $this->params;
}
}
?>
Attaquons nous aux différents moteurs. Voici en premier les 3 moteurs héritant de la classe yParamString que nous verrons en dernier.
<?php
class yModrewriteUrl extends yParamString implements iUrlEngine {
public function path2url($path) {
return 'http://'
.$_SERVER['SERVER_NAME']
.str_replace('/index.php', '', $_SERVER['SCRIPT_NAME'])
.$path;
}
}
?>
<?php
class yMultiviewsUrl extends yParamString implements iUrlEngine {
public function path2url($path) {
return 'http://'
.$_SERVER['SERVER_NAME']
.str_replace('/index.php', '/index', $_SERVER['SCRIPT_NAME'])
.$path;
}
}
?>
<?php
class yQueryStringUrl extends yParamString implements iUrlEngine {
public function path2url($path) {
return 'http://'
.$_SERVER['SERVER_NAME']
.'/'.$_SERVER['SCRIPT_NAME'].'?ps='
.$path;
}
}
?>
Plutôt creux hein ?
Et oui, comme nous l'avons vu, la param_string est identique pour les 3. Enfin presque !
En réalité, avec le multiviews, la chaîne commence par un / alors qu'avec les autres non. Ceci est traité directement dans la classe request, histoire de ne pas s'en soucier après. De plus, la sortie diffère car les urls définitives n'ont pas la même forme pour chaque, d'où le traitement dans chaque classe.
Et voici la 4ième, le cas de l'url "simple". Pour cette partie, c'est dans la classe que le choix se fait. Par exemple, au lieu de récupérer $_GET['controller'], on peut indiquer seulement $_GET['c'], on peut aussi mettre soit même sous forme d'une chaîne identifiable, enfin c'est vraiment libre.
<?php
class ySimpleUrl implements iUrlEngine {
public function url2params($url, $get) {
$params = array();
if (! isset($get['controller']) || ! isset($get['action'])) {
// Mis en commentaire pour l'exemple car valeur par défaut plus tard
//throw new Exception ("Vous devez indiquer dans l'url le controller et l'action");
} else {
$params['controller'] = $get['controller'];
$params['action'] = $get['action'];
foreach ($get as $key => $value) {
$params[$key] = urldecode($value);
}
}
return $params;
}
public function params2path($params) {
if (empty($params['controller'])) {
throw new Exception("Vous devez indiquer le controller dans vos liens");
}
if (empty($params['action'])) {
throw new Exception("Vous devez indiquer l'action dans vos liens");
}
$url_vars = array();
foreach ($params as $key => $value) {
$url_vars[] = $key.'='.urlencode($value);
}
$url_vars = implode('&', $url_vars);
return $url_vars;
}
public function path2url($path) {
return 'http://'
.$_SERVER['SERVER_NAME']
.$_SERVER['SCRIPT_NAME']
.'?'.$path;
}
}
?>
On a nos 4 moteurs, il manque l'interface que voici
<?php
interface iUrlEngine {
function url2params( $url, $get );
function params2path( $params );
function path2url( $path );
}
?>
et enfin, le gros morceau, la classe yParamString chargée de parser notre param_string (pour rappel, la param_string, c'est, dans notre cas, ceci : /application/helloworld/fr/soir)
abstract class yParamString implements iUrlEngine {
public function url2params($url, $get) {
$routes = require './app/routes.php';
if (!is_array($routes)) {
throw new Exception( "La liste des routes, logiquement dans common/config/routes.php, est vide.");
}
//print_r($routes);
$tabUrl = explode('/', $url);
foreach ($routes as $route) {
$routeUrl = substr($route['url'], 1, strlen($route['url'])-1);
// 1er test, le nombre de params
$tabRouteUrl = explode('/', $routeUrl);
if (count($tabRouteUrl) != count($tabUrl)) {
continue;
}
// ensuite, test si la regexp de l'url est bonne
preg_match_all("/:([a-zA-Z0-9]*)/", $routeUrl, $neededParams);
$url_regexp = $routeUrl;
foreach ($neededParams['1'] as $p) {
// On cherche si une regexp est definit dans la route
$regexp = (isset($route['params'][$p]) ? $route['params'][$p] : '[a-zA-Z0-9]*');
$regexp = '(?P<'.$p.'>'.$regexp.')';
$url_regexp = preg_replace("/(:".$p.")/", $regexp, $url_regexp);
}
$url_regexp = '#^'.$url_regexp.'$#i';
if (preg_match($url_regexp, $url, $matches)) {
foreach($matches as $key => $match) {
if (is_int($key)) {
unset($matches[$key]);
}
}
if (! isset($route['params'])) {
$route['params'] = array();
}
unset($get['ps']);
return array_merge($route['params'], $matches, $this->array_urldecode($get));
}
}
return $this->array_urldecode($get);
}
public function params2path($params) {
if (empty($params['controller'])) {
throw new Exception("Vous devez indiquer le controller dans vos liens");
}
if (empty($params['action'])) {
throw new Exception("Vous devez indiquer l'action dans vos liens");
}
$routes = require './app/routes.php';
$url = null;
foreach ($routes as $route) {
$badparams = false;
preg_match_all("/:([a-zA-Z0-9]*)/", $route['url'], $neededParams);
if (empty($route['params'])) {
$route['params'] = array();
}
// On vérifie pour chaque variable de l'url que la params correspondant bien
// à ce qu'on a spécifié dans le tableau params
$ValueParamsURL = array();
foreach ($neededParams['1'] as $p) {
if (isset($params[$p])) {
if (isset($route['params'][$p])) {
$regexp = $route['params'][$p];
} else {
$regexp = '[a-zA-Z0-9]*';
}
if (preg_match('#'.$regexp.'#i', $params[$p])) {
$ValueParamsURL[$p] = $params[$p];
// Le params initiale ne correspond pas à la regexp donnée par la route
} else {
$badparams = true;
//throw new Exception ('Bad regexp trouvée avec '.$p);
}
// le params existe dans l'url, mais pas de le tableau initial
} else {
$badparams = true;
}
}
if ($badparams) {
continue;
}
// On regroupe les paramètres de l'url et ceux de la route
$tabParamsURL = array_merge($route['params'], $ValueParamsURL);
// On vérifie qu'on a bien tous nos paramètres !
foreach ($tabParamsURL as $key => $value) {
if ($value != $params[$key]) {
$badparams = true;
}
}
if ($badparams) {
continue;
}
$finalTabParams = $route;
$finalNeededParams = $neededParams['1'];
$finalParamsURL = $tabParamsURL;
break;
}
// On a notre route
// On met les paramètres de l'url dans l'url
$url = $finalTabParams['url'];
foreach ($finalNeededParams as $p) {
$url = str_replace(':'.$p, $params[$p], $url);
}
// On ajoute les ?key=value en regardant ce qui n'a pas encore été mis
$getParams = array_diff_key($params, $finalParamsURL);
if (count($getParams) > 0) {
$url .= '?';
$tabTempurl = array();
foreach ($getParams as $key=>$value) {
$tabTempurl[] = $key.'='.urlencode($value);
}
$url .= implode('&', $tabTempurl);
}
return $url;
}
protected function array_urldecode($tab) {
$new_tab = array();
foreach ($tab as $key => $value) {
if (is_array($value)) {
$new_tab[$key] = self::array_urldecode($value);
} else {
$new_tab[$key] = urldecode($value);
}
}
return $new_tab;
}
}
Voici le fichier htaccess
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /framy_urlrewriting
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?ps=$1 [L,QSA]
</IfModule>
Le temps que vous digériez ce code qui prend beaucoup de place pour pas grand chose, je vais vous expliquer comment installer et utiliser l'exemple que vous pouvez télécharger par ce lien :
Télécharger le code complet
Tout d'abord, décompressez l'archive dans un répertoire accessible par un serveur web. Le framy sera contenu dans un répertoire framy_urlrewriting. Si vous renommez ce répertoire, pensez à changer aussi l'htaccess !!
Ensuite, nous allons tester nos différentes urls.
- simple : commentez toutes les lignes du htaccess et réglez la constance URL_HANDLER du index.php sur 'simple'
- query_string : commentez toutes les lignes du htaccess et réglez la constance URL_HANDLER du index.php sur 'querystring'
- multiviews : commentez toutes les lignes du htaccess et réglez la constance URL_HANDLER du index.php sur 'multiviews'
- modrewrite : décommentez toutes les lignes du htaccess et réglez la constance URL_HANDLER du index.php sur 'modrewrite'
Pour chaque type, vous pouvez ouvrir votre explorateur à l'adresse http://localhost/framy_urlrewriting.
Comme nous avons mis un comportement par défaut qui charge application/helloworld, nous arrivons dessus (voir dans la class Request).
Nous voyons un lien qui est généré par le système en fonction du type d'url. Lorsque nous clickons dessus, nous sommes renvoyé sur la même page. La différence c'est qu'en cliquant, on utilise réellement le système d'url mis en place et visible par l'url dans la barre d'adresse.
Vous verrez aussi la construction d'autres url à travers les liens fr et en dont nous avons défini les routes dans le fichier routes.php
$routes = array(
array(
'url' => '/bonsoir-le-monde',
'params' => array(
'controller' => 'application',
'action' => 'helloworld',
'lang' => 'fr',
'periode' => 'soir'
)
),
array(
'url' => '/:controller/:action/:lang/:periode',
'params' => array(
'controller' => 'application',
'action' => 'helloworld',
'lang' => 'fr|en',
'periode' => 'soir|matin|aprem|nuit'
)
),
/********************************************************
* Routes par défaut
********************************************************/
array(
'url' => '/:controller/:action/:id',
'params' => array(
'controller' => '[a-zA-Z0-9]*',
'action' => '[a-zA-Z0-9]*',
'id' => '[a-zA-Z0-9]*'
)
),
array(
'url' => '/:controller/:action',
'params' => array(
'controller' => '[a-zA-Z0-9]*',
'action' => '[a-zA-Z0-9]*',
)
)
);
ATTENTION : Classez bien vos routes de la plus restrictive à la moins restrictive, car première trouvée, premier prise. Par exemple ici, toutes nos urls peuvent utiliser /:controller/:action . Si vous mettez /bonsoir-le-monde en dessous, elle ne sera pas prise en compte.
Voila, c'est fini.
Ce morceau est bien lourd, mais nécessaire.
Prochain morceau, on crée enfin notre classe controller mère et tous ses petits périphériques.
