Search by

sugatasei / bredala-router

sugatasei

Fast request router for PHP

Package info

github.com/sugatasei/bredala-router

pkg:composer/sugatasei/bredala-router

Statistics

Installs: 21

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

3.1.1 2026-09-23 13:16 UTC

This package is auto-updated.

Last update: 2026-10-22 12:29:05 UTC


README

Routeur de requètes pour PHP : associe un couple méthode + URI à un gestionnaire, puis résout une requète entrante.

Pas de middleware, pas de conteneur d'injection, pas de groupes de routes : uniquement un registre de routes et un résolveur. Le routeur ne lit aucune superglobale, n'émet aucun header et ne produit aucune sortie.

Installation

composer require sugatasei/bredala-router

Claude Code

Ce package fournit un skill Claude Code dans skills/bredala-router/ qui documente les patterns d'usage et les pièges de la librairie (placeholders {nom} et non regex, {all} qui se déploie en plusieurs arguments, run() qui résout sans exécuter, etc.).

Dans un projet qui dépend de sugatasei/bredala-router, copie-le une fois dans .claude/skills/ après composer install pour que Claude Code le charge automatiquement (le nom du dossier doit correspondre au name déclaré dans SKILL.md) :

cp -r vendor/sugatasei/bredala-router/skills/bredala-router .claude/skills/bredala-router

Router

Bredala\Router\Router Enregistrement et résolution des routes.

Construction

  • __construct() Crée un routeur avec la table de wildcards par défaut.
  • getInstance(): static Retourne une instance partagée, créée à la demande. Sans remise à zéro : préférer new Router() dans les tests.

Enregistrement

Toutes ces méthodes sont fluides et retournent static.

  • add(string $http_method, string $uri, mixed $controller, mixed $method = null): static Enregistre une route sous une méthode quelconque.
  • get(string $uri, mixed $controller, mixed $method = null): static
  • post(string $uri, mixed $controller, mixed $method = null): static
  • getpost(string $uri, mixed $controller, mixed $method = null): static Enregistre deux routes indépendantes, en GET et en POST.
  • put(string $uri, mixed $controller, mixed $method = null): static
  • patch(string $uri, mixed $controller, mixed $method = null): static
  • delete(string $uri, mixed $controller, mixed $method = null): static
  • options(string $uri, mixed $controller, mixed $method = null): static
  • cli(string $uri, mixed $controller, mixed $method = null): static Enregistre la route sous la pseudo-méthode CLI.

Le quatrième argument décide de la nature de la route :

// $method absent -> CallableRoute, $controller est utilisé comme callable
$router->get('/ping', fn() => 'pong');
$router->get('/ping', [$service, 'method']);

// $method présent -> ControllerRoute, instanciée au moment de l'exécution
$router->get('/user/{id}', UserController::class, 'show');

Attention : le test porte sur la véracité de $method. Passer '', 0 ou null produit silencieusement une CallableRoute.

Réenregistrer le même couple méthode + URI écrase la route précédente, sans avertissement.

Wildcards

Les paramètres d'URL s'écrivent {nom}, où nom est une clé de la table de wildcards. Ce ne sont pas des expressions régulières : le motif est échappé par preg_quote() avant substitution.

  • wildcard(string $shortcut, string $pattern): static Ajoute ou remplace un wildcard. Le motif doit contenir exactement un groupe de capture.
  • wildcards(array $wildcards): static Fusionne un tableau de wildcards par-dessus les valeurs par défaut.

Table par défaut :

Placeholder Expression régulière Correspond à
{all} (.*) n'importe quoi, y compris /
{any} ([^/]+) un segment de chemin non vide
{id} ([0-9]+) entier non signé
{num} (-?[0-9]+) entier éventuellement négatif, sans décimales
{hex} ([A-Fa-f0-9]+) hexadécimal
{size} (xs|sm|md|lg) l'un de ces quatre littéraux
{uuid} ([a-fA-F0-9]{8}-…-[a-fA-F0-9]{12}) UUID canonique 8-4-4-4-12
$router->wildcard('slug', '([a-z0-9-]+)');
$router->get('/p/{slug}', PostController::class, 'show');

Un {nom} absent de la table est traité littéralement : /p/{nope} ne résout que l'URI littérale /p/{nope}.

Résolution

  • run(string $http_method, string $uri): RouteInterface Résout une requète en une route. Une méthode vide devient GET, une URI vide devient /.
  • export(): array Retourne les routes enregistrées sous la forme ['/uri' => ['GET', 'POST'], …], triées par URI.

run() cherche d'abord une correspondance exacte, puis parcourt dans l'ordre d'enregistrement les URI contenant {. Les motifs sont ancrés aux deux extrémités : /user/{id} ne correspond ni à /user/42/edit, ni à /user/42/, ni à /user/42?x=1. Il faut donc ne passer que le chemin :

$uri = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH);

Une correspondance exacte est toujours prioritaire sur un wildcard, quel que soit l'ordre d'enregistrement. Entre deux routes à wildcard, c'est la première enregistrée qui gagne.

Les méthodes HTTP sont sensibles à la casse : run('get', '/') ne trouve pas une route enregistrée par get('/').

run() résout mais n'exécute pas. C'est à l'appelant d'invoquer execute().

Paramètres capturés

Les valeurs capturées sont toujours des chaînes, transmises positionnellement dans l'ordre du motif.

$router->get('/{any}/{id}', PostController::class, 'show');
// GET /post/7 -> show('post', '7')

Chaque capture est ensuite découpée sur /, ce qui rend {all} particulier : une seule capture produit plusieurs arguments.

$router->get('/files/{all}', FileController::class, 'serve');
// GET /files/a/b/c -> serve('a', 'b', 'c')

Déclarer le gestionnaire variadique pour les recevoir :

public function serve(string ...$segments): string
{
    return implode('/', $segments);
}

Une capture vide produit un argument chaîne vide : /files/ donne [''], pas []. Les / en début et fin de capture sont supprimés : /files/a/b/ donne ['a', 'b'].

Routes

Bredala\Router\RouteInterface Interface commune, implémentée par RouteAbstract et ses deux sous-classes.

  • method(): string Retourne la méthode sous laquelle la route a été enregistrée.
  • uri(): string Retourne l'URI sous laquelle la route a été enregistrée.
  • callback(): mixed Retourne le gestionnaire brut.
  • arguments(): array Retourne les paramètres capturés.
  • setArguments(array $arguments): static Remplace les paramètres. Mute la route en place ; c'est ce qu'appelle run(), qui crée une route neuve à chaque correspondance.
  • execute(): mixed Invoque le gestionnaire et retourne sa valeur.

Bredala\Router\RouteAbstract Classe de base. Son execute() lève systématiquement une RouteException ; les deux sous-classes y retombent quand leur gestionnaire est inutilisable.

Bredala\Router\CallableRoute Exécute call_user_func_array() si le gestionnaire passe is_callable(). Accepte une closure, un nom de fonction, [$objet, 'methode'] ou [Classe::class, 'methodeStatique'].

Bredala\Router\ControllerRoute Lit $callback[0] comme nom de classe et $callback[1] comme nom de méthode, fait new $classname puis appelle la méthode avec les arguments.

L'instanciation se fait sans argument de constructeur et à chaque execute(). Il n'y a pas de conteneur d'injection : un contrôleur avec des paramètres de constructeur obligatoires lève une ArgumentCountError, et un nom de classe inconnu une Error — ni l'une ni l'autre n'est enveloppée dans une RouteException. Pour injecter des dépendances, construire l'objet soi-même et l'enregistrer comme callable :

$router->get('/user/{id}', [new UserController($repository), 'show']);

RouteException

Bredala\Router\RouteException Étend \Exception, sans code particulier. Couvre deux situations :

  • Route not found: {METHOD} {uri} — la méthode est inconnue ou aucune route ne correspond. C'est le point d'accroche pour une 404.
  • {METHOD} {uri} is not callable — le gestionnaire de la route résolue est inutilisable.

Il n'existe pas de chemin distinct pour un « 405 Method Not Allowed » : une URI qui existe sous un autre verbe est indiscernable d'une URI inexistante.

Comme les deux cas partagent la même exception, un catch englobant run() et execute() signalera une route mal configurée comme une 404. Pour les distinguer, n'encadrer que run().

Utilisation

use Bredala\Router\Router;
use Bredala\Router\RouteException;

$router = new Router();

$router->wildcard('slug', '([a-z0-9-]+)');

$router->get('/', HomeController::class, 'index');
$router->get('/user/{id}', UserController::class, 'show');
$router->post('/user', UserController::class, 'store');
$router->get('/p/{slug}', PostController::class, 'show');
$router->get('/files/{all}', FileController::class, 'serve');

$method = $_SERVER['REQUEST_METHOD'] ?? '';
$uri = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH);

try {
    echo $router->run($method, $uri)->execute();
} catch (RouteException) {
    http_response_code(404);
    echo 'Not found';
}

Tests

composer install
vendor/bin/phpunit