Le router

Résumé Support Quiz

Lorsque l'on commence à construire une vraie application PHP, l'organisation des fichiers devient rapidement importante. Un site ne se limite plus à une page : on peut avoir une page d'accueil, une page de contact, une partie blog, des templates communs, des dépendances Composer et plusieurs vues à charger suivant l'URL demandée.

L'objectif est de mettre en place une organisation plus propre, avec un dossier public, un point d'entrée unique et un système de routage capable de charger le bon contenu.

Une première organisation par fichier

L'organisation la plus naturelle consiste à créer un fichier PHP par page :

index.php contact.php blog/ index.php article.php elements/ header.php footer.php

Le fichier index.php peut alors inclure les parties communes avec require.

require 'vendor/autoload.php'; require 'elements/header.php'; ?> <h1>Bienvenue sur mon site</h1> <?php require 'elements/footer.php';

Si on utilise une dépendance installée avec Composer, comme symfony/var-dumper, il faut aussi charger l'autoloader.

composer require --dev symfony/var-dumper

Cette structure fonctionne, mais elle pose un problème important : si tout est à la racine du site, des fichiers comme composer.json ou composer.lock peuvent devenir accessibles depuis le navigateur. Ce n'est pas souhaitable, car ils exposent les dépendances et les versions utilisées par l'application.

Utiliser un dossier public

Pour éviter de rendre accessibles des fichiers internes, on crée un dossier public. Il devient la racine visible par le serveur web et ne contient que les fichiers qui doivent être accessibles publiquement.

public/ index.php contact.php blog/ index.php article.php elements/ header.php footer.php vendor/ composer.json composer.lock

Les fichiers elements, vendor et les fichiers Composer restent au-dessus du dossier public. Ils peuvent toujours être inclus par PHP, mais ils ne sont plus accessibles directement depuis une URL.

Avec le serveur interne de PHP, on peut indiquer ce dossier public avec l'option -t.

php -S localhost:8000 -t public

À partir de là, l'URL / correspond au fichier public/index.php, et /blog correspond au dossier public/blog.

Cette organisation est déjà meilleure, mais elle oblige encore à répéter les inclusions dans chaque fichier : l'autoloader, le header, le footer, avec des chemins qui changent selon la profondeur du fichier.

Centraliser avec une page carrefour

Une autre approche consiste à utiliser un seul fichier comme point d'entrée : public/index.php. Toutes les requêtes passent par ce fichier, qui décide ensuite quelle page inclure.

On peut commencer avec un paramètre d'URL simple.

$page = $_GET['page'] ?? '404'; require '../elements/header.php'; if ($page === 'blog') { require '../blog/index.php'; } elseif ($page === '404') { require '../errors/404.php'; } require '../elements/footer.php';

L'avantage est que les parties communes ne sont plus répétées dans chaque page. Le fichier index.php devient le carrefour de l'application.

Il ne faut en revanche jamais inclure directement un fichier à partir d'une valeur envoyée dans l'URL.

// À éviter absolument require $_GET['page'] . '.php';

Une personne mal intentionnée pourrait modifier le chemin pour remonter dans l'arborescence et tenter de charger des fichiers qui ne devraient pas être accessibles, comme composer.json. Il vaut mieux utiliser des conditions explicites ou un vrai système de routage.

Le principe du router

L'approche avec ?page=blog fonctionne, mais les URLs ne sont pas très propres. On préfère souvent avoir des URLs indépendantes de l'organisation des fichiers, comme :

/contact /blog/mon-super-voyage-60

Un router permet justement d'associer une URL à une action ou à un template. L'idée est de garder public/index.php comme point d'entrée, puis de détecter l'URL demandée pour charger le bon contenu.

Sans librairie, on peut déjà utiliser $_SERVER['REQUEST_URI'].

$uri = $_SERVER['REQUEST_URI']; require '../elements/header.php'; if ($uri === '/') { require '../templates/home.php'; } elseif ($uri === '/nous-contacter') { require '../templates/contact.php'; } else { echo '404'; } require '../elements/footer.php';

Cette version montre le principe, mais elle devient vite limitée dès qu'on veut gérer des URLs dynamiques.

Gérer des URLs dynamiques avec AltoRouter

Pour une URL comme /blog/mon-super-voyage-60, on doit pouvoir récupérer :

  • le slug mon-super-voyage.
  • l'identifiant 60.

Plutôt que d'écrire toute la logique à la main, on peut utiliser un router existant. Dans l'exemple, on utilise AltoRouter, qui reste simple à prendre en main.

composer require altorouter/altorouter

On initialise ensuite le routeur dans public/index.php.

require '../vendor/autoload.php'; $router = new AltoRouter(); $router->map('GET', '/', function () { echo 'Accueil'; }); $router->map('GET', '/nous-contacter', function () { echo 'Nous contacter'; }); $router->map('GET', '/blog/[*:slug]-[i:id]', function ($slug, $id) { echo "Article $slug avec l'id $id"; }); $match = $router->match();

La méthode map() permet de déclarer une route :

  • la méthode HTTP attendue, par exemple GET.
  • l'URL à reconnaître.
  • la cible à exécuter si l'URL correspond.

AltoRouter permet aussi de définir des paramètres dans l'URL. Dans /blog/[*:slug]-[i:id], slug récupère une chaîne et id récupère un entier.

Appeler la bonne cible

La méthode match() renvoie les informations de la route trouvée : la cible, les paramètres et éventuellement le nom de la route. Si aucune route ne correspond, on peut afficher une erreur 404.

Pour appeler une closure avec les paramètres détectés, on peut utiliser call_user_func_array().

$match = $router->match(); if (is_array($match)) { call_user_func_array($match['target'], $match['params']); } else { echo '404'; }

Cette fonction reçoit :

  • le callback à appeler en premier argument.
  • un tableau de paramètres à lui transmettre en second argument.

Les paramètres sont envoyés dans l'ordre où ils apparaissent dans l'URL. Si la route contient d'abord slug, puis id, la closure doit donc recevoir $slug, puis $id.

Charger des templates plutôt que des closures

On peut aussi choisir de faire pointer les routes vers des fichiers de template. Dans ce cas, la cible n'est plus forcément une closure, mais une chaîne de caractères.

$router->map('GET', '/', 'home', 'home'); $router->map('GET', '/contact', 'contact', 'contact'); $router->map('GET', '/blog/[*:slug]-[i:id]', 'blog/article', 'article'); $match = $router->match(); if (is_array($match)) { $params = $match['params']; require '../elements/header.php'; if (is_callable($match['target'])) { call_user_func_array($match['target'], $params); } else { require '../templates/' . $match['target'] . '.php'; } require '../elements/footer.php'; } else { echo '404'; }

Cette organisation permet de garder une logique simple : les routes décrivent les URLs, et les fichiers dans templates contiennent le HTML de chaque page.

Dans un template comme templates/blog/article.php, on peut ensuite utiliser les paramètres récupérés.

<h1>Article <?= $params['slug'] ?></h1>

Comme la variable $params est définie avant le require, elle reste disponible dans le fichier inclus.

Nommer les routes

AltoRouter permet aussi de donner un nom aux routes. C'est pratique pour générer des URLs sans les écrire en dur dans les templates.

$router->map('GET', '/contact', 'contact', 'contact'); $router->map('GET', '/blog/[*:slug]-[i:id]', 'blog/article', 'article');

Dans un template, on peut ensuite générer l'URL avec generate().

<a href="<?= $router->generate('contact') ?>">Nous contacter</a> <a href="<?= $router->generate('article', [ 'slug' => 'mon-super-voyage', 'id' => 60, ]) ?>"> Voir l'article </a>

L'intérêt est que l'URL réelle peut changer sans avoir à modifier tous les liens du site. Si /nous-contacter devient /contact, il suffit de modifier la déclaration de la route.

À retenir

Pour organiser une application PHP plus proprement, on va généralement :

  • créer un dossier public qui sert de racine au serveur web.
  • garder les fichiers internes, les dépendances et les templates en dehors de ce dossier public.
  • utiliser public/index.php comme point d'entrée unique.
  • router les URLs vers les bons templates ou callbacks.
  • éviter d'inclure directement un fichier à partir d'une valeur fournie dans l'URL.

Un router comme AltoRouter permet de gérer des URLs plus lisibles, des paramètres dynamiques et des noms de routes. L'organisation du code devient alors indépendante de la forme des URLs.

Ressources