chrispo/routex-php

Simple router for PHP

Maintainers

Package info

github.com/ChrispoMichigan/router-php

pkg:composer/chrispo/routex-php

Statistics

Installs: 5

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.1 2026-05-23 07:38 UTC

This package is auto-updated.

Last update: 2026-05-23 07:41:13 UTC


README

PHP Pest License

Un enrutador HTTP ligero para PHP 8.3+ con una API inspirada en Express.js.

InstalaciónInicio rápidoRutasMiddlewareRequest & ResponseErroresEjemplos

Características

  • API estilo Express.jsget(), post(), put(), patch(), delete(), options()
  • Rutas dinámicas — parámetros :id, :slug, múltiples params por ruta
  • Grupos de rutas — prefijos compartidos con group()
  • Middleware — pipeline Chain of Responsibility con use() (global) y ->middleware() (por ruta)
  • Respuestas fluentstatus(), json(), send(), html(), redirect(), header()
  • Manejo de errores — handlers personalizados para 404, 405 y excepciones globales
  • Sin dependencias — solo PHP puro, sin frameworks externos

Instalación

composer require chrispo/routex-php

Requisitos: PHP 8.3+

Inicio rápido

<?php

declare(strict_types=1);

require_once __DIR__ . '/vendor/autoload.php';

use Chrispo\RouterPhp\Request;
use Chrispo\RouterPhp\Response;
use Chrispo\RouterPhp\Router;

$router = new Router();

$router->get('/', function (Request $req, Response $res): void {
    $res->json(['message' => 'Hello World']);
});

$router->get('/users/:id', function (Request $req, Response $res): void {
    $res->json(['id' => $req->param('id')]);
});

$router->run();

Levantar el servidor:

php -S localhost:8000

Estructura de carpetas recomendada

mi-proyecto/
├── public/
│   └── index.php          # Punto de entrada
├── src/
│   ├── Controllers/       # Lógica de cada recurso
│   │   └── UserController.php
│   ├── Middleware/        # Clases de middleware propias
│   │   ├── AuthMiddleware.php
│   │   └── CorsMiddleware.php
│   └── routes.php         # Definición de todas las rutas
├── vendor/
├── composer.json
└── .env

public/index.php queda limpio:

<?php

declare(strict_types=1);

require_once __DIR__ . '/../vendor/autoload.php';

use Chrispo\RouterPhp\Router;

$router = new Router();

require_once __DIR__ . '/../src/routes.php';

$router->run();

src/routes.php concentra todas las rutas:

<?php

use Chrispo\RouterPhp\Request;
use Chrispo\RouterPhp\Response;
use App\Controllers\UserController;
use App\Middleware\AuthMiddleware;

$router->use(new CorsMiddleware());

$router->get('/users', [UserController::class, 'index']);
$router->post('/users', [UserController::class, 'store']);

$router->group('/admin', function (Router $r): void {
    $r->use(new AuthMiddleware());
    $r->get('/dashboard', [AdminController::class, 'index']);
});

Rutas

Métodos HTTP

$router->get('/users',       $handler);   // GET
$router->post('/users',      $handler);   // POST
$router->put('/users/:id',   $handler);   // PUT
$router->patch('/users/:id', $handler);   // PATCH
$router->delete('/users/:id',$handler);   // DELETE
$router->options('/users',   $handler);   // OPTIONS

// Registra el mismo handler para todos los métodos
$router->any('/ping', $handler);

Parámetros dinámicos

Usa :nombre para capturar segmentos de la URL:

$router->get('/users/:id', function (Request $req, Response $res): void {
    $id = $req->param('id');           // '42'
    $res->json(['id' => $id]);
});

// Múltiples parámetros
$router->get('/posts/:year/:month/:slug', function (Request $req, Response $res): void {
    $year  = $req->param('year');
    $month = $req->param('month');
    $slug  = $req->param('slug');
});

Query string

// GET /users?page=2&limit=5
$router->get('/users', function (Request $req, Response $res): void {
    $page  = (int) $req->query('page', '1');
    $limit = (int) $req->query('limit', '10');
});

Grupos de rutas

Agrupa rutas bajo un prefijo común. Todos los sub-handlers heredan el prefijo:

$router->group('/api/v1', function (Router $r): void {
    $r->get('/products',     $listHandler);    // GET  /api/v1/products
    $r->post('/products',    $createHandler);  // POST /api/v1/products
    $r->get('/products/:id', $showHandler);    // GET  /api/v1/products/:id
});

Middleware

El middleware sigue el patrón Chain of Responsibility: cada pieza puede modificar la petición/respuesta, llamar a $next para continuar la cadena, o cortarla devolviendo una respuesta directa.

Crear un middleware

Implementa MiddlewareInterface:

<?php

declare(strict_types=1);

use Chrispo\RouterPhp\Middleware\MiddlewareInterface;
use Chrispo\RouterPhp\Request;
use Chrispo\RouterPhp\Response;

final class AuthMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, Response $response, callable $next): void
    {
        $token = $request->header('Authorization');

        if (!str_starts_with($token, 'Bearer ')) {
            // Corta la cadena — el handler de la ruta no se ejecuta
            $response->status(401)->json(['error' => 'Unauthorized']);
            return;
        }

        $next($request, $response); // Continúa hacia el handler
    }
}

Middleware global

Se aplica a todas las rutas del router:

$router->use(new CorsMiddleware(), new LogMiddleware());

Middleware por ruta

Solo se aplica a la ruta específica. Se encadena con ->middleware():

$router->get('/reports', function (Request $req, Response $res): void {
    $res->json(['reports' => []]);
})->middleware(new AuthMiddleware());

Middleware de grupo

Dentro de group(), use() aplica solo a las rutas del grupo:

$router->group('/admin', function (Router $r): void {
    $r->use(new AuthMiddleware()); // Solo rutas /admin/*

    $r->get('/dashboard', $dashboardHandler);
    $r->get('/settings',  $settingsHandler);
});

Orden de ejecución

El pipeline respeta el orden de registro: el primer middleware registrado es el primero en ejecutarse y el último en terminar (outer → inner):

CorsMiddleware::before → AuthMiddleware::before → handler → AuthMiddleware::after → CorsMiddleware::after

Request & Response

Request — métodos disponibles

Método Descripción Ejemplo
param('key') Parámetro dinámico de ruta $req->param('id')'42'
query('key', $default) Valor de query string $req->query('page', '1')
body('key', $default) Campo del body JSON o POST $req->body('email')
body() Todo el body como array $req->body()
header('Name') Cabecera HTTP $req->header('Authorization')
all() Body + query + params fusionados $req->all()
rawBody() Body sin parsear (webhooks) $req->rawBody()
ip() IP del cliente $req->ip()
isJson() Content-Type es application/json $req->isJson()
isXhr() Petición AJAX $req->isXhr()
getMethod() Método HTTP $req->getMethod()'GET'
getPath() Path sin query string $req->getPath()'/users/42'

Response — métodos disponibles

Método Descripción Ejemplo
json($data) Serializa a JSON y envía $res->json(['id' => 1])
send($body) Envía texto plano $res->send('pong')
html($content) Envía HTML $res->html('<h1>Hello</h1>')
redirect($url, $code) Redirige (302 por defecto) $res->redirect('/login')
status($code) Establece el código HTTP $res->status(201)->json(...)
header($name, $value) Añade cabecera HTTP $res->header('X-Token', 'abc')
withHeaders($array) Añade múltiples cabeceras $res->withHeaders([...])

Los métodos status() y header() son fluent (retornan $this) y pueden encadenarse:

$res->status(201)
    ->header('X-Request-Id', uniqid())
    ->json(['created' => true]);

Manejo de errores

404 — ruta no encontrada

$router->notFound(function (Request $req, Response $res): void {
    $res->status(404)->json([
        'error'   => 'Not Found',
        'message' => sprintf('"%s %s" no existe', $req->getMethod(), $req->getPath()),
    ]);
});

405 — método no permitido

La excepción incluye los métodos permitidos para esa ruta:

use Chrispo\RouterPhp\Exceptions\MethodNotAllowedException;

$router->methodNotAllowed(function (MethodNotAllowedException $e, Request $req, Response $res): void {
    $res->status(405)
        ->header('Allow', implode(', ', $e->getAllowedMethods()))
        ->json([
            'error'   => 'Method Not Allowed',
            'allowed' => $e->getAllowedMethods(),
        ]);
});

Errores globales

Captura cualquier excepción no controlada lanzada dentro de un handler:

$router->onError(function (\Throwable $e, Request $req, Response $res): void {
    $res->status(500)->json([
        'error'   => 'Internal Server Error',
        'message' => $e->getMessage(),
    ]);
});

Ejemplos completos

El archivo index.php en la raíz del repositorio contiene ejemplos listos para ejecutar de todo lo anterior:

Sección Qué demuestra
GET / Respuesta JSON básica
GET /ping Healthcheck con texto plano
GET /users Lista con paginación via query string
POST /users Creación con validación de body JSON
GET /users/:id Parámetro dinámico
PUT /users/:id Reemplazar recurso
PATCH /users/:id Actualización parcial
DELETE /users/:id Eliminar recurso (204 sin body)
GET /api/v1/products Grupo con prefijo /api/v1
GET /posts/:year/:month/:slug Tres parámetros en una sola ruta
GET /admin/dashboard Grupo protegido con AuthMiddleware
GET /reports Middleware por ruta individual
GET /go Redirección 301
Custom 404 / 405 / error Handlers personalizados de error

Para ejecutarlo:

php -S localhost:8000
# Rutas básicas
curl http://localhost:8000/
curl http://localhost:8000/ping
curl "http://localhost:8000/users?page=2&limit=5"
curl http://localhost:8000/users/42

# Crear usuario
curl -X POST http://localhost:8000/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","email":"alice@example.com"}'

# Ruta protegida — sin token retorna 401
curl http://localhost:8000/admin/dashboard

# Ruta protegida — con token retorna 200
curl -H "Authorization: Bearer secret" http://localhost:8000/admin/dashboard

Tests

El proyecto incluye 91 tests con Pest:

php vendor/bin/pest

Benchmarks

Mediciones de rendimiento con PHPBench:

php vendor/bin/phpbench run benchmarks/ --report=aggregate

Resultados de referencia (PHP 8.5, sin opcache):

Benchmark Tiempo Descripción
Ruta estática ~2 µs Caso base
Ruta dinámica ~2.5 µs +25% por regex
50 rutas registradas ~17 µs O(n) lineal
Con 3 middlewares ~3 µs +45% overhead pipeline
Sin middlewares ~2 µs Baseline
Ruta no encontrada ~1.5 µs Loop + excepción