Routeur PHP

Créez un système de routage efficace pour vos applications web PHP

Introduction aux routeurs PHP

Un routeur est un composant essentiel dans le développement d'applications web modernes. Il permet de diriger les requêtes HTTP vers les contrôleurs et actions appropriés en fonction de l'URL demandée. Un routeur bien conçu facilite la mise en place d'une architecture MVC propre et l'organisation de votre code.

Traditionnellement, en PHP, les URL utilisaient des paramètres GET explicites (comme page.php?id=5&action=edit). Avec un routeur, vous pouvez transformer ces URL en formats plus propres et intuitifs comme /articles/5/edit. Le routeur interprète cette URL et détermine quelle partie de votre application doit traiter cette requête.

Un système de routage vous permet également de centraliser la gestion des requêtes HTTP à travers un point d'entrée unique (généralement index.php), ce qui améliore considérablement la sécurité et la maintenabilité de votre application.

Pourquoi utiliser un routeur ?

  • URLs propres et SEO-friendly : Les moteurs de recherche préfèrent les URL sans paramètres visibles
  • Séparation des responsabilités : Division claire entre le routage, les contrôleurs et la logique métier
  • Structure évolutive : Facilite l'ajout de nouvelles fonctionnalités sans modifier le code existant
  • APIs RESTful : Simplification de la création d'APIs suivant les conventions REST
  • Sécurité renforcée : Contrôle précis des points d'entrée et filtrage des requêtes
  • Middlewares : Possibilité d'intercaler des traitements avant ou après l'exécution des routes
  • Tests simplifiés : Architecture plus facile à tester de manière unitaire et fonctionnelle

La plupart des frameworks PHP modernes (Laravel, Symfony, Slim, etc.) intègrent des systèmes de routage sophistiqués. Dans ce module, nous allons comprendre le fonctionnement interne d'un routeur en créant notre propre implémentation simple mais complète.

Structure de base d'un routeur

La mise en place d'un routeur PHP repose sur quelques principes fondamentaux :

  1. Point d'entrée unique : Toutes les requêtes sont dirigées vers un seul fichier (généralement index.php)
  2. Configuration serveur : Le serveur web (Apache, Nginx) est configuré pour rediriger toutes les requêtes vers ce point d'entrée
  3. Analyse de l'URL : Le routeur extrait et analyse l'URL demandée
  4. Matching de routes : Le routeur compare l'URL aux routes définies pour trouver une correspondance
  5. Exécution : Si une correspondance est trouvée, le contrôleur et l'action associés sont exécutés
Structure de fichiers

Ce code illustre le point d'entrée unique de l'application, généralement placé dans le dossier public/. Ce fichier est responsable de l'initialisation du routeur, du chargement des routes définies dans un fichier séparé et du dispatching des requêtes vers les contrôleurs appropriés.

// /public/index.php

// Chargement de l'autoloader
require_once '../vendor/autoload.php';

// Initialisation du routeur
$router = new \App\Router\Router();

// Chargement des routes
require_once '../config/routes.php';

// Exécution du routeur avec la requête actuelle
$router->dispatch();

Configuration du serveur web

Pour que le routeur fonctionne correctement, vous devez configurer votre serveur web pour rediriger toutes les requêtes vers le point d'entrée :

Apache (.htaccess) :

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]

Nginx :

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

Cette configuration indique au serveur de rediriger toutes les requêtes vers index.php, sauf si la requête correspond à un fichier ou dossier existant.

Base d'un routeur simple

Commençons par implémenter un routeur simple qui nous permettra de comprendre les mécanismes fondamentaux du routage en PHP. Ce routeur de base prendra en charge :

La classe Router est le cœur du système. Elle stocke toutes les routes définies dans un tableau associatif et fournit les méthodes nécessaires pour ajouter et dispatcher des routes.

Classe Router
Voici l'implémentation centrale du routeur. Cette classe gère l'enregistrement des routes pour différentes méthodes HTTP (GET, POST), le stockage des callbacks associés, et le dispatching des requêtes vers les contrôleurs appropriés. Notez la structure de données en deux dimensions qui organise les routes par méthode HTTP puis par chemin.
// /src/Router/Router.php

namespace App\Router;

class Router
{
    protected array $routes = [];
    
    /**
     * Ajoute une route GET
     * @param string $path - Le chemin de la route (ex: "articles", "articles/:id")
     * @param mixed $callback - La fonction/méthode à exécuter quand cette route est atteinte
     */
    public function get(string $path, $callback): void
    {
        $this->addRoute('GET', $path, $callback);
    }
    
    /**
     * Ajoute une route POST
     * @param string $path - Le chemin de la route
     * @param mixed $callback - Peut être une closure ou un tableau [ControllerClass, 'method']
     */
    public function post(string $path, $callback): void
    {
        $this->addRoute('POST', $path, $callback);
    }
    /**
     * Ajoute une route avec une méthode HTTP spécifique
     * 
     * Cette méthode interne est utilisée par get(), post(), etc. pour enregistrer les routes
     * dans la structure de données du routeur, organisée par méthode HTTP et chemin
     * 
     * @param string $method - Méthode HTTP (GET, POST, PUT, DELETE...)
     * @param string $path - Chemin de la route
     * @param mixed $callback - Fonction ou méthode à exécuter
     */
    protected function addRoute(string $method, string $path, $callback): void
    {        // Normalisation du chemin (suppression des slashes au début/fin)
        $path = trim($path, '/');
        
        // Stockage de la route dans un tableau associatif à deux dimensions
        // Structure: $routes[MÉTHODE][CHEMIN] = CALLBACK
        $this->routes[$method][$path] = $callback;
    }
      /**
     * Dispatch la requête vers la bonne route
     * 
     * Cette méthode est le cœur du routeur. Elle analyse l'URL courante,
     * trouve la route correspondante et exécute le callback associé.
     */
    public function dispatch(): void
    {
        // 1. Récupération de la méthode HTTP (GET, POST, etc.)
        $method = $_SERVER['REQUEST_METHOD'];        
        
        // 2. Récupération et nettoyage de l'URI demandée        
        // - Récupère l'URI depuis $_SERVER['REQUEST_URI'] 
        // - Supprime les paramètres GET (tout ce qui suit '?')
        // - Supprime les slashes au début et à la fin
        $uri = $_SERVER['REQUEST_URI'] ?? '/';
        $uri = explode('?', $uri)[0];
        $uri = trim($uri, '/');
        
        // 3. Recherche de la route correspondante dans le tableau des routes
        // Vérifie si une route correspond exactement à la méthode HTTP et au chemin demandé
        if (isset($this->routes[$method][$uri])) {
            $callback = $this->routes[$method][$uri];
              // 4. Exécution du callback associé à la route
            if (is_callable($callback)) {
                // Si c'est une fonction anonyme ou une callable directe
                echo call_user_func($callback);
            } else if (is_array($callback) && count($callback) === 2) {
                // Si c'est un tableau au format [ControllerClass, 'method']
                list($controller, $action) = $callback;
                
                // Si le contrôleur est spécifié sous forme de nom de classe, l'instancie
                if (is_string($controller)) {
                    $controller = new $controller();
                }
                
                // Exécute la méthode du contrôleur
                echo $controller->$action();
            }
            
            return;
        }
          // 5. Si aucune route ne correspond, renvoie une erreur 404
        http_response_code(404);
        echo '404 - Page non trouvée';
    }
}
Définition des routes

Cet exemple montre comment définir différentes routes dans un fichier de configuration centralisé. Chaque route est associée à un contrôleur et à une méthode spécifique qui sera exécutée lorsque l'URL correspondante est demandée. Notez les différentes méthodes HTTP (get, post) qui permettent de distinguer les actions selon le type de requête. Les routes peuvent également être gérées par des fonctions anonymes pour des cas simples comme les API.

// /config/routes.php

// Routes de l'application
$router->get('', [App\Controllers\HomeController::class, 'index']);
$router->get('about', [App\Controllers\HomeController::class, 'about']);
$router->get('contact', [App\Controllers\HomeController::class, 'contact']);
$router->post('contact', [App\Controllers\HomeController::class, 'submitContact']);

// Route avec fonction anonyme
$router->get('api/status', function() {
    header('Content-Type: application/json');
    return json_encode(['status' => 'ok', 'version' => '1.0.0']);
});

Paramètres dynamiques dans les routes

Un routeur vraiment utile doit pouvoir gérer des paramètres dynamiques dans les URLs. Par exemple, pour une page d'article ayant l'URL /articles/123, nous devons pouvoir extraire l'ID "123" et le transmettre au contrôleur.

Les paramètres dynamiques sont généralement définis avec un préfixe spécial (comme : ou {}) dans la définition de la route. Par exemple, /articles/:id:id est un paramètre qui peut prendre n'importe quelle valeur.

Pour implémenter cette fonctionnalité, notre routeur doit :

  1. Détecter les routes contenant des paramètres dynamiques (commençant par :)
  2. Convertir ces routes en expressions régulières pour permettre la correspondance
  3. Extraire les valeurs des paramètres dynamiques de l'URL demandée
  4. Passer ces valeurs en arguments au callback de la route

Cette approche permet de créer des URLs expressives et bien structurées, tout en maintenant un code propre et modulaire.

Routes avec paramètres
// Exemple d'utilisation avec des paramètres

// Route avec un paramètre ID
$router->get('users/:id', [UserController::class, 'show']);

// Route avec plusieurs paramètres
$router->get('blog/:year/:month/:slug', [BlogController::class, 'show']);
Gestion des paramètres dynamiques

Cette partie du code montre comment améliorer notre routeur pour qu'il prenne en charge les paramètres dynamiques dans les URL. L'implémentation utilise des expressions régulières pour transformer les segments d'URL avec :paramètre en motifs de correspondance capables d'extraire les valeurs. Les trois fonctions clés sont convertRouteToRegex, extractParams et executeRoute, qui travaillent ensemble pour faire correspondre l'URL demandée à une définition de route et exécuter le code approprié avec les paramètres extraits.

// Amélioration du Router pour gérer les paramètres

class Router
{
    // ... Code précédent ...
    
    /**
     * Dispatch la requête avec support des paramètres dynamiques
     */
    public function dispatch(): void
    {
        $method = $_SERVER['REQUEST_METHOD'];
        $uri = trim(explode('?', $_SERVER['REQUEST_URI'])[0], '/');
        
        // D'abord, essayez de trouver une correspondance exacte
        if (isset($this->routes[$method][$uri])) {
            $this->executeRoute($this->routes[$method][$uri]);
            return;
        }
        
        // Si pas de correspondance exacte, cherchez des routes avec paramètres
        foreach ($this->routes[$method"] as $route => $callback) {
        foreach ($this->routes[$method] as $route => $callback) {
            // Vérifiez si la route contient des paramètres dynamiques
            if (strpos($route, ':') !== false) {
                $routeRegex = $this->convertRouteToRegex($route);
                
                if (preg_match($routeRegex, $uri, $matches)) {
                    // Extraire les valeurs des paramètres
                    $params = $this->extractParams($route, $uri);
                    
                    // Exécuter la route avec les paramètres
                    $this->executeRoute($callback, $params);
                    return;
                }
            }
        }
        
        // Route non trouvée
        http_response_code(404);
        echo '404 - Page non trouvée';
    }
    
    /*     
    Convertit une route avec paramètres en expression régulière
    */
    protected function convertRouteToRegex(string $route): string
    {
        // Remplace :param par un groupe de capture regex
        $routeRegex = preg_replace('/:[a-zA-Z0-9]+/', '([^/]+)', $route);
        
        // Ajoute les délimiteurs et ancres
        return '@^' . $routeRegex . '$@D';
    }
    
    /*
    Extrait les valeurs des paramètres de l'URI
    */
    protected function extractParams(string $route, string $uri): array
    {
        $params = [];
        $routeParts = explode('/', $route);
        $uriParts = explode('/', $uri);
        
        foreach ($routeParts as $index => $part) {
            if (strpos($part, ':') === 0) {
                // Extraction du nom du paramètre (retire le ':')
                $paramName = substr($part, 1);
                
                // Récupération de la valeur depuis l'URI
                $params[$paramName] = $uriParts[$index];
            }
        }
        
        return $params;
    }
    
    /*
    Exécute une route avec ses paramètres éventuels
    */
    protected function executeRoute($callback, array $params = []): void
    {
        if (is_callable($callback)) {
            echo call_user_func_array($callback, $params);
        } else if (is_array($callback) && count($callback) === 2) {
            list($controller, $action) = $callback;
            
            if (is_string($controller)) {
                $controller = new $controller();
            }
            
            echo call_user_func_array([$controller, $action], $params);
        }
    }
}

Ce diagramme illustre le processus d'extraction des paramètres dynamiques dans un routeur PHP. Quand une URL contient des segments variables (marqués par : dans la définition de la route), le routeur identifie ces segments et extrait leurs valeurs. Ces valeurs sont ensuite accessibles comme paramètres dans votre contrôleur, permettant de créer des URLs flexibles et sémantiques. Cette technique est essentielle pour construire des applications RESTful où les identifiants et autres variables font partie de l'URL elle-même.

/blog/:year/:month/:slug
URL demandée
/blog/2025/06/php-router
Extraction des paramètres
Paramètres:
year = 2025
month = 06
slug = php-router

Gestion des méthodes HTTP

Un routeur complet doit prendre en charge toutes les méthodes HTTP standard (GET, POST, PUT, DELETE, PATCH, etc.). Chaque méthode a une signification particulière dans le contexte des API RESTful :

Notre routeur doit permettre de définir des routes pour chacune de ces méthodes et d'exécuter le code approprié en fonction de la méthode utilisée pour accéder à une URL. Il est important de comprendre que l'idempotence signifie qu'exécuter la même requête plusieurs fois produit le même résultat.

Comprendre l'idempotence des méthodes HTTP

L'idempotence est un concept clé en développement web :

  • GET, PUT, DELETE : Idempotentes - Exécuter la même requête plusieurs fois a le même effet
  • POST, PATCH : Non idempotentes - Chaque exécution peut avoir un effet différent

Par exemple : Supprimer un article (DELETE) peut être exécuté plusieurs fois sans problème, mais créer un nouvel article (POST) créera un nouvel article à chaque fois.

Il faut également gérer le cas particulier des navigateurs qui ne supportent nativement que GET et POST. Pour permettre l'utilisation des autres méthodes (PUT, DELETE, etc.) dans les formulaires HTML, nous pouvons utiliser une technique appelée "method override" qui consiste à utiliser un champ caché _method ou un en-tête HTTP spécial X-HTTP-Method-Override.

Définition de routes avec différentes méthodes
// Exemple de définition de routes pour différentes méthodes HTTP

// Route GET pour afficher un formulaire de création
$router->get('users/create', [UserController::class, 'create']);

// Route POST pour traiter la création (non idempotente)
$router->post('users', [UserController::class, 'store']);

// Route GET pour afficher un utilisateur spécifique
$router->get('users/:id', [UserController::class, 'show']);

// Route GET pour afficher le formulaire d'édition
$router->get('users/:id/edit', [UserController::class, 'edit']);

// Route PUT pour mettre à jour complètement une ressource (idempotente)
$router->put('users/:id', [UserController::class, 'update']);

// Route PATCH pour mise à jour partielle (généralement non idempotente)
$router->patch('users/:id', [UserController::class, 'partialUpdate']);

// Route DELETE pour supprimer une ressource (idempotente)
$router->delete('users/:id', [UserController::class, 'destroy']);

// Route OPTIONS pour CORS (Cross-Origin Resource Sharing)
$router->options('users/:id', function() {
    header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
    header('Access-Control-Allow-Headers: Content-Type, Authorization');
    return '';
});

// Route qui répond à plusieurs méthodes
$router->match(['GET', 'POST'], 'users/profile', [UserController::class, 'profile']);
Implémentation complète des méthodes HTTP

Cette classe Router étendue implémente toutes les méthodes HTTP standard (GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD) ainsi que des méthodes utilitaires comme match() et any(). L'implémentation inclut la fonction detectMethod() qui gère automatiquement le "method spoofing" pour permettre l'utilisation de méthodes comme PUT ou DELETE dans les formulaires HTML qui ne supportent nativement que GET et POST. Cette approche est fondamentale pour construire des APIs RESTful complètes.

class Router
{
    // ...existing code...
    
    /**
     * Ajoute une route PUT
     * PUT est utilisé pour remplacer complètement une ressource
     * @param string $path - Le chemin de la route
     * @param mixed $callback - Le callback à exécuter
     */
    public function put(string $path, $callback): void
    {
        $this->addRoute('PUT', $path, $callback);
    }
    
    /**
     * Ajoute une route DELETE
     * DELETE est idempotente - supprimer la même ressource plusieurs fois a le même effet
     * @param string $path - Le chemin de la route
     * @param mixed $callback - Le callback à exécuter
     */
    public function delete(string $path, $callback): void
    {
        $this->addRoute('DELETE', $path, $callback);
    }
    
    /**
     * Ajoute une route PATCH
     * PATCH est utilisé pour des mises à jour partielles
     * @param string $path - Le chemin de la route
     * @param mixed $callback - Le callback à exécuter
     */
    public function patch(string $path, $callback): void
    {
        $this->addRoute('PATCH', $path, $callback);
    }
    
    /**
     * Ajoute une route OPTIONS
     * Principalement utilisé pour les requêtes CORS preflight
     * @param string $path - Le chemin de la route
     * @param mixed $callback - Le callback à exécuter
     */
    public function options(string $path, $callback): void
    {
        $this->addRoute('OPTIONS', $path, $callback);
    }
    
    /**
     * Ajoute une route HEAD
     * Similaire à GET mais retourne seulement les en-têtes
     * @param string $path - Le chemin de la route     * @param mixed $callback - Le callback à exécuter
     */
    public function head(string $path, $callback): void
    {
        $this->addRoute('HEAD', $path, $callback);
    }
    
    /**     * Ajoute une route qui répond à plusieurs méthodes HTTP
     * Utile pour des routes qui doivent traiter plusieurs types de requêtes
     * @param array $methods - Tableau des méthodes HTTP acceptées
     * @param string $path - Le chemin de la route
     * @param mixed $callback - Le callback à exécuter
     */
    public function match(array $methods, string $path, $callback): void
    {
        foreach ($methods as $method) {
            $this->addRoute(strtoupper($method), $path, $callback);
        }
    }
    
    /**     * Ajoute une route pour toutes les méthodes HTTP
     * Attention : À utiliser avec parcimonie car cela peut créer des comportements inattendus
     * @param string $path - Le chemin de la route
     * @param mixed $callback - Le callback à exécuter
     */public function any(string $path, $callback): void
    {
        $methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
        $this->match($methods, $path, $callback);
    }
    
    /**
     * Détecte la méthode HTTP réelle (pour supporter PUT, DELETE, etc.)
     * Cette méthode gère le "method spoofing" pour les navigateurs qui ne supportent que GET et POST
     * @return string La méthode HTTP détectée     */
    protected function detectMethod(): string
    {
        $method = $_SERVER['REQUEST_METHOD'];
        
        // Détecte si une méthode HTTP est simulée via un champ _method dans POST
        // Utile pour les formulaires HTML qui ne supportent que GET et POST
        if ($method === 'POST' && isset($_POST['_method'])) {
            $overrideMethod = strtoupper($_POST['_method']);
            
            // Valide que la méthode override est supportée
            if (in_array($overrideMethod, ['PUT', 'PATCH', 'DELETE'])) {
                return $overrideMethod;
            }
        }
          // Détecte si une méthode HTTP est spécifiée dans l'en-tête HTTP_X_HTTP_METHOD_OVERRIDE
        // Standard utilisé par certaines bibliothèques JavaScript
        if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
            $overrideMethod = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
            
            // Valide la méthode
            if (in_array($overrideMethod, ['PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'])) {
                return $overrideMethod;
            }
        }
        
        return $method;
    }
    
    /**
     * Met à jour la méthode dispatch pour utiliser detectMethod()
     */
    public function dispatch(): void
    {
        // Utilise la méthode améliorée de détection
        $method = $this->detectMethod();
        
        // ...existing code...
    }
}
Exemple d'utilisation avec Method Override dans un formulaire

Les navigateurs web ne supportent nativement que les méthodes HTTP GET et POST pour les formulaires HTML. Pour contourner cette limitation et utiliser d'autres méthodes comme PUT, PATCH ou DELETE, on utilise la technique du "Method Override". Cet exemple illustre comment simuler une requête DELETE en utilisant un formulaire POST standard avec un champ caché _method qui indique la véritable méthode à utiliser. Le routeur détectera ce champ et traitera la requête comme si elle avait été envoyée avec la méthode spécifiée.


<form method="POST" action="/users/123">
    
    <input type="hidden" name="_method" value="DELETE">
    
    <p>Êtes-vous sûr de vouloir supprimer cet utilisateur ?</p>
    <button type="submit">Confirmer la suppression</button>
</form>


<form method="POST" action="/users/123">
    <input type="hidden" name="_method" value="PUT">
    
    <label>Nom:
        <input type="text" name="name" value="Jean Dupont">
    </label>    
    <label>Email:
        <input type="email" name="email" value="jean@example.com">
    </label>
    
    <button type="submit">Mettre à jour</button>
Exemple de contrôleur RESTful complet

Ce contrôleur illustre parfaitement l'approche RESTful pour la gestion des ressources avec un routeur PHP moderne. Chaque méthode correspond à une action CRUD spécifique associée à une route et à une méthode HTTP précise :

  • index() - Récupère et affiche une collection de ressources (GET /users)
  • create() - Affiche un formulaire de création (GET /users/create)
  • store() - Traite la création d'une nouvelle ressource (POST /users)
  • show() - Affiche une ressource spécifique (GET /users/{id})
  • edit() - Affiche un formulaire d'édition (GET /users/{id}/edit)
  • update() - Traite la mise à jour complète (PUT /users/{id})
  • partialUpdate() - Traite la mise à jour partielle (PATCH /users/{id})
  • destroy() - Supprime une ressource (DELETE /users/{id})

Notez les commentaires concernant l'idempotence (une même requête produit toujours le même résultat) et la sûreté (ne modifie pas l'état du serveur) - des concepts essentiels dans la conception d'API RESTful.

/**
 * Contrôleur User suivant les conventions RESTful
 * Chaque méthode correspond à une action CRUD standard
 */
class UserController
{
    protected $userRepository;
    
    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }
    
    /**
     * GET /users - Affiche la liste de tous les utilisateurs
     * Idempotente et sûre
     */
    public function index()
    {
        $users = $this->userRepository->findAll();
        return new View('users/index', ['users' => $users]);
    }
    
    /**
     * GET /users/create - Affiche le formulaire de création
     * Idempotente et sûre
     */
    public function create()
    {
        return new View('users/create');
    }
    
    /**
     * POST /users - Traite la création d'un nouvel utilisateur
     * Non idempotente - chaque appel crée un nouvel utilisateur
     */
    public function store($request)
    {
        // Validation des données
        $validator = new Validator($request->post());
        $validator->required(['name', 'email'])
                  ->email('email')
                  ->unique('email', 'users');
        
        if (!$validator->isValid()) {
            return new RedirectResponse('/users/create', ['errors' => $validator->getErrors()]);
        }
        
       // Création de l'utilisateur
        $user = $this->userRepository->create($validator->getData());
        
        return new RedirectResponse("/users/{$user->id}", ['success' => 'Utilisateur créé avec succès']);
    }
    
    /**
     * GET /users/:id - Affiche un utilisateur spécifique
     * Idempotente et sûre
     */
    
    public function show($request)
    {
        $user = $this->userRepository->find($request->params['id']);
        
        if (!$user) {
            throw new RouterException('Utilisateur non trouvé', 404);
        }
        
        return new View('users/show', ['user' => $user]);
    }    
    /**
     * GET /users/:id/edit - Affiche le formulaire d'édition
     * Idempotente et sûre
     */
    
    public function edit($request)
    {
        $user = $this->userRepository->find($request->params['id']);
        
        if (!$user) {
            throw new RouterException('Utilisateur non trouvé', 404);
        }
        
        return new View('users/edit', ['user' => $user]);
    }    
    /**
     * PUT /users/:id - Met à jour complètement un utilisateur
     * Idempotente - exécuter plusieurs fois avec les mêmes données produit le même résultat
     */
    
    public function update($request)
    {
        $user = $this->userRepository->find($request->params['id']);
        
        if (!$user) {
            throw new RouterException('Utilisateur non trouvé', 404);
        }
        
        // Validation
        $validator = new Validator($request->post());
        $validator->required(['name', 'email'])
                  ->email('email')
                  ->unique('email', 'users', $user->id);
        
        if (!$validator->isValid()) {
            return new RedirectResponse("/users/{$user->id}/edit", ['errors' => $validator->getErrors()]);
        }
          // Mise à jour complète de l'utilisateur
        $this->userRepository->update($user->id, $validator->getData());
        
        return new RedirectResponse("/users/{$user->id}", ['success' => 'Utilisateur mis à jour']);
    }
        /**
     * PATCH /users/:id - Met à jour partiellement un utilisateur
     * Généralement non idempotente (dépend de l'implémentation)
     */
    
    public function partialUpdate($request)
    {
        $user = $this->userRepository->find($request->params['id']);
        
        if (!$user) {
            throw new RouterException('Utilisateur non trouvé', 404);
        }
                // Validation uniquement des champs présents
        
        $data = $request->post();
        $allowedFields = ['name', 'email', 'phone', 'address'];
        $updateData = array_intersect_key($data, array_flip($allowedFields));
        
        if (empty($updateData)) {
            throw new RouterException('Aucune donnée valide fournie', 400);
        }
                // Mise à jour partielle
        
        $this->userRepository->partialUpdate($user->id, $updateData);
        
        return new JsonResponse(['message' => 'Utilisateur mis à jour partiellement']);
    }
        /**
     * DELETE /users/:id - Supprime un utilisateur
     * Idempotente - supprimer plusieurs fois le même utilisateur a le même effet
     */
    
    public function destroy($request)
    {
        $user = $this->userRepository->find($request->params['id']);
        
        if (!$user) {
            // Pour DELETE, on peut considérer que supprimer un élément inexistant est "réussi"
            // car l'objectif (l'élément n'existe plus) est atteint
            return new RedirectResponse('/users', ['info' => 'Utilisateur déjà supprimé']);
        }
                // Vérification des contraintes (ex: ne pas supprimer un admin)
        
        if ($user->role === 'admin' && $this->userRepository->countAdmins() <= 1) {
            throw new RouterException('Impossible de supprimer le dernier administrateur', 403);
        }
        
        $this->userRepository->delete($user->id);
        
        return new RedirectResponse('/users', ['success' => 'Utilisateur supprimé']);
    }
}

Support des méthodes HTTP dans les navigateurs

Bien que les navigateurs ne prennent en charge nativement que GET et POST, il existe plusieurs approches courantes pour gérer PUT, PATCH et DELETE :

  1. Headers HTTP (AJAX) : Dans les requêtes JavaScript, vous pouvez spécifier la méthode directement via fetch() ou XMLHttpRequest
  2. Paramètre _method : Pour les formulaires HTML normaux, ajoutez un champ caché
  3. En-tête X-HTTP-Method-Override : Standard utilisé par certaines bibliothèques et API

Les trois approches sont implémentées dans notre méthode detectMethod() ci-dessus.

Conventions RESTful pour les URLs

Voici les conventions standard pour organiser vos routes RESTful :

Méthode URL Action Description
GET /users index Afficher tous les utilisateurs
GET /users/create create Formulaire de création
POST /users store Créer un nouvel utilisateur
GET /users/:id show Afficher un utilisateur
GET /users/:id/edit edit Formulaire d'édition
PUT /users/:id update Mettre à jour complètement
PATCH /users/:id partialUpdate Mettre à jour partiellement
DELETE /users/:id destroy Supprimer l'utilisateur

Ces conventions rendent votre API prévisible et facilitent sa compréhension par d'autres développeurs.

Groupes de routes

Définition de groupes de routes

Les groupes de routes sont une fonctionnalité puissante qui permet d'organiser logiquement les routes et d'appliquer des attributs communs (préfixes, middlewares, espaces de noms) à un ensemble de routes. Cette organisation simplifie considérablement la gestion des routes dans une application de taille importante.

Dans cet exemple, le préfixe admin est ajouté à toutes les routes du groupe, créant ainsi un espace d'administration isolé et cohérent. D'autres attributs courants pour les groupes incluent :

  • Middlewares partagés (authentification, vérification de rôles)
  • Namespace de contrôleurs commun
  • Préfixes d'URL ou de nom de route
  • Contraintes de domaine

Les groupes peuvent également être imbriqués pour créer des hiérarchies complexes de routes avec héritage des attributs.

// Exemple de définition de groupes de routes
$router = new Router();

// Groupe de routes avec préfixe "admin"
$router->group(['prefix' => 'admin'], function($router) {
    // Ces routes seront accessibles via /admin/users et /admin/settings
    $router->get('users', [AdminController::class, 'listUsers']);
    $router->get('settings', [AdminController::class, 'showSettings']);
});

// Groupe avec préfixe et middleware
$router->group([
    'prefix' => 'api',
    'middleware' => [AuthMiddleware::class, ApiRateLimitMiddleware::class]
], function($router) {
    $router->get('users', [ApiController::class, 'getUsers']);
    $router->post('users', [ApiController::class, 'createUser']);
});
Implémentation des groupes de routes

Cette implémentation de la méthode group() permet d'organiser les routes en groupes logiques qui partagent des attributs communs comme un préfixe d'URL, des middlewares, ou un namespace de contrôleur. La méthode utilise une pile (stack) pour gérer l'imbrication des groupes. Lorsqu'un nouveau groupe est créé, ses attributs sont fusionnés avec ceux du groupe parent, puis les routes définies à l'intérieur du callback héritent automatiquement de ces attributs. Cela permet une organisation hiérarchique et modulaire des routes tout en réduisant la duplication de code.

class Router
{
    protected $groupStack = [];
    protected $routes = [];
    
    // ... autres méthodes ...
    
    
    /**
     * Crée un groupe de routes
     */
    public function group(array $attributes, callable $callback): void
    {
        // Sauvegarde l'état actuel du groupe
        $this->updateGroupStack($attributes);
        
        // Exécute le callback pour définir les routes du groupe
        $callback($this);
        
        // Restaure l'état précédent
        array_pop($this->groupStack);
    }
    
    /**
     * Mise à jour de la pile des groupes
     */
    protected function updateGroupStack(array $attributes): void
    {
        if (count($this->groupStack) > 0) {
            $attributes = $this->mergeWithLastGroup($attributes);
        }
        
        $this->groupStack[] = $attributes;
    }
    
    /**
     * Fusionne les attributs avec le groupe précédent
     */
    protected function mergeWithLastGroup(array $new): array
    {        $last = end($this->groupStack);
          // Fusion des préfixes
        if (isset($new['prefix']) && isset($last['prefix'])) {
            $new['prefix'] = $last['prefix'] . '/' . trim($new['prefix'], '/');
        }
          // Fusion des middlewares
        if (isset($last['middleware'])) {
            $middlewares = isset($new['middleware']) ? $new['middleware'] : [];
            $new['middleware'] = array_merge(
                (array) $last['middleware'],
                (array) $middlewares
            );
        }
        
        return array_merge($last, $new);
    }    
    /**
     * Ajoute une route en tenant compte des groupes
     */
    
    protected function addRoute(string $method, string $path, $callback): void
    {
        // Applique les attributs de groupe si nécessaire        if (!empty($this->groupStack)) {
            $group = end($this->groupStack);
            
            // Applique le préfixe du groupe
            if (isset($group['prefix'])) {
                $path = trim($group['prefix'], '/') . '/' . trim($path, '/');
            }
            
            // Stocke les middlewares du groupe
            $middlewares = isset($group['middleware']) ? (array) $group['middleware'] : [];
        } else {
            $middlewares = [];
        }
        
        $this->routes[] = [
            'method' => strtoupper($method),
            'path' => '/' . trim($path, '/'),
            'callback' => $callback,
            'middleware' => $middlewares        ];
    }
}

                    

Avantages des groupes de routes

Les groupes de routes offrent plusieurs avantages :

  • Organisation logique des routes liées
  • Réduction de la duplication de code pour les préfixes communs
  • Application de middlewares à un ensemble de routes
  • Possibilité d'imbriquer des groupes pour une hiérarchie complexe

C'est une pratique essentielle pour les applications de moyenne à grande taille.

Système de middlewares avancé

Les middlewares sont des couches de traitement qui s'exécutent avant et/ou après le traitement principal d'une requête. Ils permettent d'implémenter des fonctionnalités transversales comme l'authentification, la journalisation, la limitation de débit, la validation CSRF, la gestion CORS, etc.

Un middleware suit le pattern "Pipeline" où chaque middleware peut :

  • Traitement avant : Exécuter du code avant de passer au middleware suivant
  • Délégation : Passer la requête au middleware suivant via $next($request)
  • Traitement après : Exécuter du code après le retour du middleware suivant
  • Court-circuit : Arrêter l'exécution en ne pas appelant $next
  • Modification : Modifier la requête ou la réponse
Middlewares fondamentaux

Voici plusieurs implémentations de middlewares essentiels pour une application web moderne et sécurisée. Le middleware AuthMiddleware gère l'authentification des utilisateurs en vérifiant leur session et en redirigeant les utilisateurs non authentifiés. Le LogMiddleware trace chaque requête avec des informations détaillées comme la durée d'exécution et le code de statut. Le RoleMiddleware contrôle les accès basés sur les rôles des utilisateurs. Enfin, le RateLimitMiddleware protège votre application contre les abus en limitant le nombre de requêtes par période de temps. Chaque middleware suit le pattern "Pipeline" où la méthode handle() peut traiter la requête avant et après l'exécution du reste de la chaîne.

/**
 * Middleware d'authentification avec vérification de session
 */
class AuthMiddleware
{
    /**
     * Vérifie si l'utilisateur est authentifié
     * @param Request $request La requête HTTP
     * @param Closure $next La fonction à exécuter ensuite
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        // Démarre la session si pas déjà fait
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        
        // Vérifie si l'utilisateur est connecté
        if (!isset($_SESSION['user_id']) || empty($_SESSION['user_id'])) {
            // Stocke l'URL de destination pour redirection après connexion
            $_SESSION['intended_url'] = $_SERVER['REQUEST_URI'];
            
            // Gestion différente selon le type de requête
            if ($request->isAjax()) {
                http_response_code(401);
                header('Content-Type: application/json');
                echo json_encode(['error' => 'Non authentifié', 'redirect' => '/login']);
                exit;
            }
              // Redirection classique
            header('Location: /login');
            exit;
        }
          // Vérification de l'expiration de session
        if (isset($_SESSION['last_activity']) && 
            (time() - $_SESSION['last_activity'] > 3600)) { // 1 heure
            session_destroy();
            header('Location: /login?expired=1');
            exit;
        }
        
        // Met à jour l'activité
        $_SESSION['last_activity'] = time();
        
        // Ajoute l'utilisateur à la requête pour usage ultérieur
        $request->setUser(new User($_SESSION['user_id']));
        
        // Continue l'exécution
        return $next($request);
    }
}

/**
 * Middleware de journalisation avancé
 */
class LogMiddleware
{
    protected $logger;
    
    public function __construct(Logger $logger = null)
    {
        $this->logger = $logger ?? new FileLogger('/var/log/app.log');
    }
    
    public function handle($request, Closure $next)
    {
              // Données de la requête
              $startTime = microtime(true);
              $method = $_SERVER['REQUEST_METHOD'];
              $uri = $_SERVER['REQUEST_URI'];
              $ip = $request->getClientIp();
              $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown';
              
              // Log de début de requête
              $this->logger->info("[{$method}] {$uri} - IP: {$ip}", [
                  'user_agent' => $userAgent,
                  'timestamp' => date('Y-m-d H:i:s')
              ]);
              
              try {
                  // Exécute le reste de la chaîne
                  $response = $next($request);
                  
                  // Log de succès
                  $duration = round((microtime(true) - $startTime) * 1000, 2);
                  $statusCode = http_response_code() ?: 200;
                  
                  $this->logger->info("[{$method}] {$uri} - {$statusCode} - {$duration}ms");
                  
                  return $response;
                  
              } catch (\Exception $e) {
                  // Log d'erreur
                  $duration = round((microtime(true) - $startTime) * 1000, 2);
                  $this->logger->error("[{$method}] {$uri} - ERROR - {$duration}ms", [
                      'exception' => $e->getMessage(),
                      'file' => $e->getFile(),
                      'line' => $e->getLine(),
                      'trace' => $e->getTraceAsString()
                  ]);
                  
                  throw $e; // Re-lance l'exception
              }
          }
      }      /**
       * Middleware de contrôle d'accès basé sur les rôles
       */
      class RoleMiddleware
      {
          protected $requiredRoles;
          
          public function __construct($roles = [])
          {
              $this->requiredRoles = is_array($roles) ? $roles : [$roles];
          }
          
          public function handle($request, Closure $next)
          {
              $user = $request->getUser();
              
              if (!$user) {
                  http_response_code(401);
                  throw new RouterException('Authentification requise', 401);
              }
              
              // Vérification des rôles
              if (!empty($this->requiredRoles)) {
                  $userRoles = $user->getRoles();
                  $hasRequiredRole = false;
                  
                  foreach ($this->requiredRoles as $role) {
                      if (in_array($role, $userRoles)) {
                          $hasRequiredRole = true;
                          break;
                      }
                  }
                  
                  if (!$hasRequiredRole) {
                      http_response_code(403);
                      throw new RouterException('Accès non autorisé', 403);
                  }
              }
              
              return $next($request);
          }
      }      /**
       * Middleware de limitation du taux de requêtes (Rate Limiting)
       */
      class RateLimitMiddleware
      {
          protected $maxRequests;
          protected $timeWindow; // en secondes
          protected $storage; // Redis, fichier, etc.
          
          public function __construct(int $maxRequests = 60, int $timeWindow = 60)
          {
              $this->maxRequests = $maxRequests;
              $this->timeWindow = $timeWindow;
              $this->storage = new FileStorage('/tmp/rate_limit'); // Exemple simple
          }
          
          public function handle($request, Closure $next)
          {
              $identifier = $this->getIdentifier($request);
              $key = 'rate_limit:' . $identifier . ':' . floor(time() / $this->timeWindow);
              
              // Récupère le nombre de requêtes pour cette fenêtre
              $requests = (int) $this->storage->get($key, 0);
              
              if ($requests >= $this->maxRequests) {
                  // Limite dépassée
                  http_response_code(429);
                  header('Retry-After: ' . $this->timeWindow);
                  header('X-RateLimit-Limit: ' . $this->maxRequests);
                  header('X-RateLimit-Remaining: 0');
                  
                  throw new RouterException('Trop de requêtes', 429);
              }
              
              // Incrémente le compteur
              $this->storage->increment($key, $this->timeWindow);
              
              // Ajoute les en-têtes de limite
              header('X-RateLimit-Limit: ' . $this->maxRequests);
              header('X-RateLimit-Remaining: ' . ($this->maxRequests - $requests - 1));
              
              return $next($request);
          }
          
          protected function getIdentifier($request): string
          {
              // Peut être l'IP, l'ID utilisateur, une clé API, etc.
              $user = $request->getUser();
              if ($user) {
                  return 'user_' . $user->getId();
              }
              
              return 'ip_' . $request->getClientIp();
          }
      }
Application des middlewares

Cet exemple montre comment intégrer les middlewares dans le routeur. La méthode middleware() permet d'ajouter des middlewares globaux qui s'appliqueront à toutes les routes de l'application. Ces middlewares sont stockés dans un tableau et seront exécutés dans l'ordre où ils ont été ajoutés. Cette conception utilise le pattern de chaînage de méthodes (fluent interface) en retournant $this, ce qui permet d'enchaîner plusieurs appels de méthode pour une syntaxe plus concise et lisible lors de la configuration du routeur.

class Router
{
    protected $routes = [];
    protected $globalMiddlewares = [];
    
    // ... autres méthodes ...
    
    /**
     * Ajoute un middleware global
     */
    public function middleware($middleware): self
    {
        $this->globalMiddlewares[] = $middleware;
        return $this;
    }
    
    /**
     * Ajoute un middleware à une route spécifique
     */
    protected function addRouteMiddleware(string $path, $middleware): void
    {
        
        // Trouve la dernière route ajoutée et ajoute le middleware        
        foreach ($this->routes as &$route) {
            if ($route['path'] === $path) {
                if (!isset($route['middleware'])) {
                    $route['middleware'] = [];                }
                $route['middleware'] = array_merge($route['middleware'], (array) $middleware);
                break;
            }
        }
    }
    
    /**
     * Exécute les middlewares dans l'ordre
     */
    protected function runMiddlewares(array $middlewares, $request, Closure $target)
    {
        // Si aucun middleware, exécute directement la cible
        if (empty($middlewares)) {
            return $target($request);
        }
        
        // Crée une chaîne de middlewares (pattern pipeline)
        $pipeline = array_reduce(
            array_reverse($middlewares),
            function ($next, $middleware) {
                return function ($request) use ($middleware, $next) {
                    // Instancie le middleware si c'est un nom de classe
                    if (is_string($middleware)) {
                        $middleware = new $middleware();
                    }
                    return $middleware->handle($request, $next);
                };
            },
            $target
        );
        
        return $pipeline($request);
    }
    
    /**
     * Traitement de la requête
     */
    public function run()
    {
        $method = $_SERVER['REQUEST_METHOD'];
        $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
        
        foreach ($this->routes as $route) {
            if ($route['method'] === $method && $this->matchRoute($route['path'], $uri, $params)) {
                $request = new Request($params);
                
                // Combine les middlewares globaux avec ceux de la route
                $middlewares = array_merge(
                    $this->globalMiddlewares,
                    $route['middleware'] ?? []
                );
                
                // Prépare la cible finale
                $target = function ($request) use ($route) {
                    // Exécute le contrôleur ou la closure
                    if (is_array($route['callback']) && count($route['callback']) === 2) {
                        $controller = new $route['callback'][0]();
                        return $controller->{$route['callback'][1]}($request);
                    }
                    return call_user_func($route['callback'], $request);
                };
                
                // Exécute la chaîne de middlewares
                return $this->runMiddlewares($middlewares, $request, $target);
            }
        }
        
        // Aucune route trouvée
        throw new \Exception('Route non trouvée', 404);
    }
    
    /**
     * Gère une exception
     */
    protected function handleException(RouterException $exception)
    {
        $statusCode = $exception->getStatusCode();
        
        http_response_code($statusCode);
        
        // Utilise un gestionnaire d'erreur personnalisé si défini
        if (isset($this->errorHandlers[$statusCode])) {
            return call_user_func($this->errorHandlers[$statusCode], $exception);
        }        // Gestionnaire par défaut
        if ($statusCode === 404) {
            return "404 - Page non trouvée" . $exception->getMessage()  ;
        }
        
        return "Erreur " . $statusCode .  . $exception->getMessage() ;
    }
}
Utilisation des gestionnaires d'erreurs

Cet exemple montre comment définir des gestionnaires d'erreurs personnalisés pour les codes d'erreur HTTP courants comme 404 (ressource non trouvée) et 500 (erreur serveur interne). La gestion centralisée des erreurs permet une expérience utilisateur cohérente tout en facilitant la journalisation et le débogage. Notez comment les exceptions sont utilisées pour signaler différentes conditions d'erreur, avec la possibilité d'inclure des vues personnalisées pour chaque type d'erreur. Le routeur capture ces exceptions et délègue le traitement aux gestionnaires appropriés.

$router = new Router();

// Définir des gestionnaires d'erreurs personnalisés
$router->error(404, function() {
    include 'views/404.php';
    return null;
});

$router->error(500, function(RouterException $e) {
    error_log("Erreur critique: {$e->getMessage()}\n{$e->getTraceAsString()}");
    include 'views/500.php';
    return null;
});

// Ajout de routes avec des contrôleurs potentiellement problématiques
$router->get('produits/:id', function($request) {
    if (!is_numeric($request->params['id'])) {
        throw new \InvalidArgumentException('ID de produit invalide');
    }
      $produit = $productRepository->find($request->params['id']);
    
    if ($produit === null) {
        throw new RouterException('Produit non trouvé', 404);
    }
    
    return new ProductView($produit);
});

Bonnes pratiques pour la gestion des erreurs

Une bonne gestion des erreurs est essentielle pour une application robuste :

  • Utilisez des codes HTTP appropriés (404 pour "non trouvé", 403 pour "interdit", etc.)
  • Créez des pages d'erreur personnalisées pour améliorer l'expérience utilisateur
  • Journalisez les erreurs graves pour pouvoir les analyser
  • Évitez d'exposer des informations sensibles dans les messages d'erreur en production
  • Différenciez les messages d'erreur entre environnements de développement et de production

En production, affichez des messages d'erreur conviviaux pour l'utilisateur tout en enregistrant les détails techniques pour le débogage.

Injection de dépendances

Contrôleur avec injection de dépendances

Ce code illustre l'utilisation de l'injection de dépendances dans les contrôleurs, un principe fondamental de conception orientée objet. Au lieu de créer des dépendances à l'intérieur du contrôleur, celui-ci reçoit ses dépendances via son constructeur. Cette approche présente plusieurs avantages : le code est plus facilement testable (on peut injecter des mocks), le couplage est réduit entre les composants, et les responsabilités sont clairement séparées. Dans cet exemple, le contrôleur reçoit un repository pour l'accès aux données et un logger pour enregistrer les événements, ce qui lui permet de se concentrer uniquement sur la logique de contrôle.

class UserController
{
    protected $userRepository;
    protected $logger;
    
    /**
     * Injection des dépendances via le constructeur
     */
    public function __construct(UserRepository $userRepository, Logger $logger)
    {
        $this->userRepository = $userRepository;
        $this->logger = $logger;
    }
    
    public function index()
    {
        $users = $this->userRepository->findAll();
        $this->logger->info('Liste des utilisateurs consultée');
          return new View('users/index', ['users' => $users]);
    }
    
    public function show($request)
    {
        $user = $this->userRepository->find($request->params['id']);
        
        if (!$user) {
            throw new RouterException('Utilisateur non trouvé', 404);
        }
        
        $this->logger->info("Utilisateur {$user->id} consulté");
        
        return new View('users/show', ['user' => $user]);
    }
    
    /**
     * GET /users/:id - Affiche un utilisateur spécifique
     * Idempotente et sûre
     */    public function show($request)
    {
        $user = $this->userRepository->find($request->params['id']);
        
        if (!$user) {
            throw new RouterException('Utilisateur non trouvé', 404);
        }
        
        return new View('users/show', ['user' => $user]);
    }
    
    /**
     * GET /users/:id/edit - Affiche le formulaire d'édition
     * Idempotente et sûre
     */    public function edit($request)
    {
        $user = $this->userRepository->find($request->params['id']);
        
        if (!$user) {
            throw new RouterException('Utilisateur non trouvé', 404);
        }
        
        return new View('users/edit', ['user' => $user]);
    }
    
    /**
     * PUT /users/:id - Met à jour complètement un utilisateur
     * Idempotente - exécuter plusieurs fois avec les mêmes données produit le même résultat
     */    public function update($request)
    {
        $user = $this->userRepository->find($request->params['id']);
        
        if (!$user) {
            throw new RouterException('Utilisateur non trouvé', 404);
        }
        
        // Validation
        $validator = new Validator($request->post());
        $validator->required(['name', 'email'])
                  ->email('email')
                  ->unique('email', 'users', $user->id);
        
        if (!$validator->isValid()) {
            return new RedirectResponse("/users/{$user->id}/edit", ['errors' => $validator->getErrors()]);
        }
        
        // Mise à jour complète de l'utilisateur
        $this->userRepository->update($user->id, $validator->getData());
        
        return new RedirectResponse("/users/{$user->id}", ['success' => 'Utilisateur mis à jour']);
    }
    
    /**
     * PATCH /users/:id - Met à jour partiellement un utilisateur
     * Généralement non idempotente (dépend de l'implémentation)
     */    public function partialUpdate($request)    {
        $user = $this->userRepository->find($request->params['id']);
        
        if (!$user) {
            throw new RouterException('Utilisateur non trouvé', 404);
        }
        
        // Validation uniquement des champs présents
        $data = $request->post();
        $allowedFields = ['name', 'email', 'phone', 'address'];
        $updateData = array_intersect_key($data, array_flip($allowedFields));
        
        if (empty($updateData)) {
            throw new RouterException('Aucune donnée valide fournie', 400);
        }
        
        // Mise à jour partielle
        $this->userRepository->partialUpdate($user->id, $updateData);
        
        return new JsonResponse(['message' => 'Utilisateur mis à jour partiellement']);
    }
    
    /**
     * DELETE /users/:id - Supprime un utilisateur
     * Idempotente - supprimer plusieurs fois le même utilisateur a le même effet
     */    public function destroy($request)
    {
        $user = $this->userRepository->find($request->params['id']);
        
        if (!$user) {            // Pour DELETE, on peut considérer que supprimer un élément inexistant est "réussi"
            // car l'objectif (l'élément n'existe plus) est atteint
            return new RedirectResponse('/users', ['info' => 'Utilisateur déjà supprimé']);
        }
        
        // Vérification des contraintes (ex: ne pas supprimer un admin)
        if ($user->role === 'admin' && $this->userRepository->countAdmins() <= 1) {
            throw new RouterException('Impossible de supprimer le dernier administrateur', 403);
        }
        
        $this->userRepository->delete($user->id);
        
        return new RedirectResponse('/users', ['success' => 'Utilisateur supprimé']);
    }
}

Support des méthodes HTTP dans les navigateurs

Bien que les navigateurs ne prennent en charge nativement que GET et POST, il existe plusieurs approches courantes pour gérer PUT, PATCH et DELETE :

  1. Headers HTTP (AJAX) : Dans les requêtes JavaScript, vous pouvez spécifier la méthode directement via fetch() ou XMLHttpRequest
  2. Paramètre _method : Pour les formulaires HTML normaux, ajoutez un champ caché
  3. En-tête X-HTTP-Method-Override : Standard utilisé par certaines bibliothèques et API

Les trois approches sont implémentées dans notre méthode detectMethod() ci-dessus.

Conventions RESTful pour les URLs

Voici les conventions standard pour organiser vos routes RESTful :

Méthode URL Action Description
GET /users index Afficher tous les utilisateurs
GET /users/create create Formulaire de création
POST /users store Créer un nouvel utilisateur
GET /users/:id show Afficher un utilisateur
GET /users/:id/edit edit Formulaire d'édition
PUT /users/:id update Mettre à jour complètement
PATCH /users/:id partialUpdate Mettre à jour partiellement
DELETE /users/:id destroy Supprimer l'utilisateur

Ces conventions rendent votre API prévisible et facilitent sa compréhension par d'autres développeurs.

Routes nommées et génération d'URLs

Un système de routage vraiment puissant doit permettre la génération d'URLs à partir des routes définies. Au lieu de coder en dur les URLs dans vos vues et contrôleurs, vous pouvez utiliser des routes nommées pour générer automatiquement les liens corrects.

Cette approche présente plusieurs avantages majeurs :

  • Maintenabilité : Modifier l'URL d'une route ne nécessite pas de chercher tous les liens dans le code
  • Consistance : Évite les erreurs de frappe dans les URLs
  • Refactoring : Facilite la restructuration des URLs de l'application
  • Localisation : Permet d'avoir des URLs différentes selon la langue
  • Environnements : Adapte automatiquement les URLs selon l'environnement (dev, prod)
  • HTTPS/HTTP : Gère automatiquement le protocole approprié
Définition de routes nommées

Les routes nommées permettent de référencer les chemins d'URL par un nom symbolique plutôt que par leur chemin brut. Cette approche offre une abstraction qui découple le code de la structure exacte des URLs. Dans cet exemple, chaque route est définie avec la méthode name() pour lui attribuer un identifiant unique. Les conventions de nommage suivent généralement une structure hiérarchique (par exemple, blog.index, blog.show) qui reflète l'organisation des ressources. Les routes nommées sont particulièrement utiles pour générer des URLs dans les vues et les redirections, car elles permettent de modifier le chemin d'une route sans avoir à mettre à jour tous les liens qui y font référence.

// Définition de routes avec noms

// Routes principales
$router->get('', [HomeController::class, 'index'])->name('home');
$router->get('about', [PageController::class, 'about'])->name('about');
$router->get('contact', [ContactController::class, 'show'])->name('contact');
$router->post('contact', [ContactController::class, 'submit'])->name('contact.submit');

// Routes de blog avec paramètres
$router->get('blog', [BlogController::class, 'index'])->name('blog.index');
$router->get('blog/:slug', [BlogController::class, 'show'])
       ->whereSlug('slug')
       ->name('blog.show');

// Routes d'utilisateurs (CRUD complet)
$router->get('users', [UserController::class, 'index'])->name('users.index');
$router->get('users/create', [UserController::class, 'create'])->name('users.create');
$router->post('users', [UserController::class, 'store'])->name('users.store');
$router->get('users/:id', [UserController::class, 'show'])
       ->whereNumber('id')
       ->name('users.show');
$router->get('users/:id/edit', [UserController::class, 'edit'])
       ->whereNumber('id')
       ->name('users.edit');
$router->put('users/:id', [UserController::class, 'update'])
       ->whereNumber('id')
       ->name('users.update');
$router->delete('users/:id', [UserController::class, 'destroy'])
       ->whereNumber('id')
       ->name('users.destroy');

// Routes avec paramètres multiples
$router->get('categories/:category/products/:id', [ProductController::class, 'show'])
       ->whereSlug('category')
       ->whereNumber('id')
       ->name('products.show');

// Routes avec paramètres optionnels (via plusieurs définitions)
$router->get('search', [SearchController::class, 'index'])->name('search');
$router->get('search/:query', [SearchController::class, 'results'])
       ->where('query', '[^/]+')
       ->name('search.results');
Gestionnaire d'URLs (URL Generator)

Le générateur d'URL est un composant essentiel d'un système de routage avancé qui permet de générer des liens dynamiquement à partir des routes nommées. Cette approche offre plusieurs avantages majeurs :

  • Centralisation de la définition des URLs dans un seul endroit (les routes)
  • Modification facile des URLs sans avoir à mettre à jour tous les liens dans l'application
  • Construction automatique des paramètres dynamiques dans les URLs
  • Gestion des URLs absolues et relatives selon le contexte

Ce générateur s'utilise typiquement via des fonctions d'aide (route('nom.route', ['param' => 'valeur'])) dans les vues pour créer des liens cohérents avec les routes définies.

Utiliser systématiquement le générateur d'URL plutôt que des URLs codées en dur permet de maintenir une application robuste face aux changements de structure.

/**
 * Générateur d'URLs basé sur les routes nommées
 */
class UrlGenerator
{
    protected $routes = [];
    protected $baseUrl;
    protected $scheme;
      public function __construct(array $routes = [], string $baseUrl = null)
    {
        $this->routes = $routes;
        $this->baseUrl = $baseUrl ?? $this->detectBaseUrl();
        $this->scheme = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
    }
    
    /**
     * Génère une URL à partir d'un nom de route
     * @param string $name - Nom de la route
     * @param array $params - Paramètres à injecter dans l'URL
     * @param array $query - Paramètres de query string (?param=value)
     * @param bool $absolute - Générer une URL absolue ou relative
     * @return string URL générée
     */
    public function route(string $name, array $params = [], array $query = [], bool $absolute = false): string
    {
        if (!isset($this->routes[$name])) {
            throw new \InvalidArgumentException("Route '{$name}' non trouvée");
        }
        
        $route = $this->routes[$name];
        $path = $route->getPath();
        
        // Remplace les paramètres dans le chemin
        $url = $this->replaceParameters($path, $params);
        
        // Ajoute les paramètres de query string
        if (!empty($query)) {
            $url .= '?' . http_build_query($query);
        }        
        // Retourne une URL absolue ou relative
        if ($absolute) {
            return $this->scheme . '://' . $this->baseUrl . '/' . ltrim($url, '/');
        }
        
        return '/' . ltrim($url, '/');
    }
        /**
     * Génère une URL absolue
     */
    public function routeAbsolute(string $name, array $params = [], array $query = []): string
    {
        return $this->route($name, $params, $query, true);
    }
        /**
     * Remplace les paramètres dans le chemin de la route
     */
    protected function replaceParameters(string $path, array $params): string
    {
        // Trouve tous les paramètres :nom dans le chemin
        preg_match_all('/:([\w]+)/', $path, $matches);
        
        $missingParams = [];
        
        foreach ($matches[1] as $paramName) {
            if (!array_key_exists($paramName, $params)) {
                $missingParams[] = $paramName;
                continue;
            }            
            // Remplace :paramName par la valeur
            $path = str_replace(':' . $paramName, $params[$paramName], $path);
        }
        
        if (!empty($missingParams)) {
            throw new \InvalidArgumentException(
                'Paramètres manquants pour générer l\'URL : ' . implode(', ', $missingParams)
            );
        }
        
        return $path;
    }
        /**
     * Détecte l'URL de base automatiquement
     */
    protected function detectBaseUrl(): string
    {
        $host = $_SERVER['HTTP_HOST'] ?? 'localhost';
        $scriptName = dirname($_SERVER['SCRIPT_NAME']);
        $basePath = rtrim(str_replace('\\', '/', $scriptName), '/');
        
        return $host . $basePath;
    }
        /**
     * Ajoute des routes au générateur
     */
    public function addRoute(string $name, Route $route): void
    {
        $this->routes[$name] = $route;
    }
        /**
     * Génère une URL pour un asset (CSS, JS, images)
     */
    public function asset(string $path, bool $absolute = false): string
    {
        $url = '/assets/' . ltrim($path, '/');
        
        if ($absolute) {
            return $this->scheme . '://' . $this->baseUrl . $url;
        }
        
        return $url;
    }
        /**
     * Vérifie si une route existe
     */
    public function hasRoute(string $name): bool
    {
        return isset($this->routes[$name]);
    }
        /**
     * Récupère toutes les routes nommées
     */
    public function getAllRoutes(): array
    {
        return $this->routes;
    }
}
Intégration avec le routeur

Cette partie du code montre comment intégrer le mécanisme des routes nommées au cœur du routeur. La classe Router est étendue pour stocker les routes avec leurs noms dans un tableau associatif $namedRoutes. La méthode addRoute() est modifiée pour retourner l'instance de Route créée, ce qui permet le chaînage de méthodes et notamment l'appel à la méthode name(). Ce pattern de conception permet d'ajouter des fonctionnalités aux routes de manière fluide et expressive. C'est une démonstration pratique de l'extension d'un système existant de manière à préserver la rétrocompatibilité tout en ajoutant de nouvelles capacités.

class Router
{
    protected $routes = [];    protected $namedRoutes = [];
    protected $urlGenerator;
    
    // ...existing code...
    
    /**
     * Modifie addRoute pour gérer les routes nommées
     */
    protected function addRoute(string $method, string $path, $callback): Route
    {
        $route = new Route($method, $path, $callback);
        $this->routes[] = $route;
          return $route;
    }
    
    /**
     * Enregistre une route nommée
     */
    public function registerNamedRoute(string $name, Route $route): void
    {
        if (isset($this->namedRoutes[$name])) {
            throw new \InvalidArgumentException("Route '{$name}' déjà définie");
        }
        
        $this->namedRoutes[$name] = $route;
        
        // Ajoute la route au générateur d'URLs
        $this->getUrlGenerator()->addRoute($name, $route);
    }
    
    /**
     * Récupère le générateur d'URLs
     */    public function getUrlGenerator(): UrlGenerator
    {
        if ($this->urlGenerator === null) {
            $this->urlGenerator = new UrlGenerator($this->namedRoutes);
        }
        
        return $this->urlGenerator;
    }
    
    /**
     * Génère une URL à partir d'un nom de route (méthode raccourci)
     */    public function url(string $name, array $params = [], array $query = []): string
    {
        return $this->getUrlGenerator()->route($name, $params, $query);
    }
    
    /**
     * Récupère une route par son nom
     */    public function getRoute(string $name): ?Route
    {
        return $this->namedRoutes[$name] ?? null;
    }
    
    /**
     * Vérifie si une route nommée existe
     */    public function hasRoute(string $name): bool
    {
        return isset($this->namedRoutes[$name]);
    }
}

/**
 * Modification de la classe Route pour gérer les noms
 */
class Route
{
    // ...existing code...
    
    /**
     * Définit le nom de la route et l'enregistre dans le routeur
     */
    public function name(string $name): self
    {
        $this->name = $name;
        
        // Si on a accès au routeur, enregistre la route nommée
        if (isset($this->router)) {
            $this->router->registerNamedRoute($name, $this);
        }
        
        return $this;
    }
}
Utilisation dans les vues et contrôleurs

Cet exemple démontre l'utilisation pratique du générateur d'URL dans un contrôleur. Le routeur est injecté dans le contrôleur via le constructeur, ce qui permet d'accéder facilement à toutes ses fonctionnalités. La méthode url() du routeur est utilisée pour générer dynamiquement l'URL vers la page de détail d'un utilisateur après sa création. Cette approche présente plusieurs avantages : elle évite de coder en dur les chemins d'URL, permet de modifier la structure des URLs sans impacter le code, et automatise la génération des URLs avec paramètres en remplaçant les segments dynamiques par les valeurs correspondantes (ici l'ID de l'utilisateur).

// Dans un contrôleur
class UserController
{    protected $router;
    
    public function __construct(Router $router)
    {
        $this->router = $router;
    }
    
    public function store($request)
    {
        // Logique de création de l'utilisateur...
          $user = $this->userRepository->create($validatedData);
        
        // Redirection vers la page de l'utilisateur créé
        $redirectUrl = $this->router->url('users.show', ['id' => $user->id]);
        
        return new RedirectResponse($redirectUrl, [
            'success' => 'Utilisateur créé avec succès'
        ]);
    }
      public function edit($request)    {
        $user = $this->userRepository->find($request->params['id']);
        
        // Génération d'URLs pour la vue
        $urls = [
            'update' => $this->router->url('users.update', ['id' => $user->id]),
            'show' => $this->router->url('users.show', ['id' => $user->id]),
            'index' => $this->router->url('users.index')
        ];
        
        return new View('users/edit', [
            'user' => $user,
            'urls' => $urls
        ]);
    }
}
Helper global pour les vues

Pour simplifier l'accès aux fonctionnalités du routeur dans les vues, ce code met en place un système de fonctions helpers globales. Une variable globale $globalRouter est utilisée pour stocker l'instance du routeur, et la fonction setGlobalRouter() permet d'initialiser cette instance. Cette approche présente l'avantage de rendre les fonctionnalités du routeur facilement accessibles depuis n'importe quel template sans avoir à passer explicitement l'instance du routeur. Les helpers globaux comme route(), url() ou is_current_route() sont particulièrement utiles dans les templates où l'on cherche à garder un code concis et lisible.

// Fonction helper globale (à inclure dans functions.php)

/**
 * Instance globale du routeur
 */
$globalRouter = null;

/**
 * Définit le routeur global */
function setGlobalRouter(Router $router): void
{
    global $globalRouter;
    $globalRouter = $router;
}

/**
 * Génère une URL à partir d'un nom de route
 */
function route(string $name, array $params = [], array $query = []): string
{
    global $globalRouter;
    
    if ($globalRouter === null) {
        throw new \RuntimeException('Router global non défini');
    }
    
    return $globalRouter->url($name, $params, $query);
}

/**
 * Génère une URL absolue
 */
function route_absolute(string $name, array $params = [], array $query = []): string
{
    global $globalRouter;
    
    return $globalRouter->getUrlGenerator()->routeAbsolute($name, $params, $query);
}

/**
 * Génère une URL d'asset
 */
function asset(string $path, bool $absolute = false): string
{
    global $globalRouter;
    
    return $globalRouter->getUrlGenerator()->asset($path, $absolute);
}

/**
 * Vérifie si la route actuelle correspond au nom donné
 */
function is_current_route(string $name): bool
{
    // Cette fonction nécessiterait de stocker la route courante
    // dans une variable globale lors du dispatch
    global $currentRoute;
    
    return $currentRoute && $currentRoute->getName() === $name;
}
Utilisation dans les templates HTML

Cet exemple montre l'utilisation concrète des helpers de routage dans un template HTML. Le menu de navigation utilise route() pour générer dynamiquement les URLs des différentes sections du site, et is_current_route() pour ajouter une classe CSS "active" à l'élément correspondant à la page actuelle. Cette approche offre plusieurs avantages : le code est plus lisible et concis, les URLs sont générées dynamiquement (évitant les erreurs de frappe ou les liens cassés lors de modifications), et la navigation active est gérée automatiquement. Le même principe peut s'appliquer aux formulaires, boutons et autres liens à travers l'application, garantissant une cohérence dans la génération des URLs.


<nav class="main-navigation">
    <ul>
        <li class="<?= is_current_route('home') ? 'active' : '' ?>">
            <a href="<?= route('home') ?>">Accueil</a>
        </li>
        <li class="<?= is_current_route('blog.index') ? 'active' : '' ?>">
            <a href="<?= route('blog.index') ?>">Blog</a>
        </li>
        <li class="<?= is_current_route('about') ? 'active' : '' ?>">
            <a href="<?= route('about') ?>">À propos</a>
        </li>
        <li class="<?= is_current_route('contact') ? 'active' : '' ?>">
            <a href="<?= route('contact') ?>">Contact</a>
        </li>
    </ul>
</nav>


<form method="POST" action="<?= route('contact.submit') ?>">
    <label>Nom:
        <input type="text" name="name" required>
    </label>
      <label>Email:
        <input type="email" name="email" required>
    </label>
    
    <label>Message:
        <textarea name="message" required></textarea>
    </label>
    
    <button type="submit">Envoyer</button>
</form>


<div class="user-list">    <?php foreach ($users as $user): ?>
        <div class="user-card">
            <h3><a href="<?= route('users.show', ['id' => $user->id]) ?>">
                <?= htmlspecialchars($user->name) ?>
            </a></h3>
            
            <p><?= htmlspecialchars($user->email) ?></p>
            
            <div class="actions">
                <a href="<?= route('users.edit', ['id' => $user->id]) ?>" class="btn btn-primary">Modifier</a>
                
                <form method="POST" action="<?= route('users.destroy', ['id' => $user->id]) ?>" style="display: inline;">
                    <input type="hidden" name="_method" value="DELETE">                    <button type="submit" class="btn btn-danger" onclick="return confirm('Êtes-vous sûr ?')">
                        Supprimer                    </button>                </form>
            </div>
        </div>
    
</div>


<div class="pagination">
    
        <a href="<?= route('users.index', [], ['page' => $currentPage - 1]) ?>">« Précédent</a>
    
    
    Page <?= $currentPage ?> sur <?= $totalPages ?>
    
    
        <a href="<?= route('users.index', [], ['page' => $currentPage + 1]) ?>">Suivant »</a>
    
</div>


<link rel="stylesheet" href="<?= asset('css/main.css') ?>">
<script src="<?= asset('js/app.js') ?>"></script>
                    

Avantages des routes nommées

Le système de routes nommées apporte de nombreux bénéfices :

  • Maintenance facilitée : Modifier une URL ne nécessite que de changer la définition de route
  • Refactoring sûr : Les IDE peuvent détecter les utilisations d'une route nommée
  • Évite les erreurs : Plus de risque d'erreur de frappe dans les URLs
  • Auto-complétion : Les IDE peuvent proposer l'auto-complétion des noms de routes
  • Documentation vivante : Les noms de routes documentent l'intention
  • URLs cohérentes : Assure la cohérence du format des URLs dans toute l'application
  • Environnements multiples : S'adapte automatiquement selon l'environnement

Conventions de nommage des routes

Adoptez des conventions claires pour nommer vos routes :

  • Format ressource.action : users.index, users.show, users.create
  • Actions CRUD standard :
    • index - Liste des ressources
    • show - Affichage d'une ressource
    • create - Formulaire de création
    • store - Traitement de la création
    • edit - Formulaire d'édition
    • update - Traitement de la mise à jour
    • destroy - Suppression
  • Hiérarchie : admin.users.index pour les routes d'administration
  • API : api.v1.users.show pour les routes d'API
  • Cohérence : Utilisez toujours le même format dans toute l'application

© Tutoriel PHP - Tous droits réservés