elliephp / routing
A minimal, fast routing component for ElliePHP API framework based on FastRoute and PSR-7/PSR-15 standards
Installs: 24
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
pkg:composer/elliephp/routing
Requires
- php: ^8.4
- nikic/fast-route: ^1.3
- nyholm/psr7: ^1.8
- psr/container: ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
Requires (Dev)
- giorgiosironi/eris: ^1.0
- phpunit/phpunit: ^11.0
README
A minimal, fast routing component for ElliePHP API framework based on FastRoute and PSR-7/PSR-15 standards.
Installation
composer require elliephp/routing
Key Features
- Fast routing built on nikic/fast-route
- Multiple caching layers for high-performance applications
- PSR-7 and PSR-15 compliant
- Support for closures, controllers, and callable handlers
- PSR-15 middleware with proper stack execution
- Route groups with shared prefixes and middleware
- Domain routing for multi-tenant applications
- Production-ready route caching
- PHP 8.4+ with strict types
Breaking Changes in v2.0
The error handling architecture has changed from internal handling to middleware-based handling for better PSR-15 compliance.
Before (v1.x):
$router = new Routing(...); $response = $router->handle($request); // Router caught errors internally
After (v2.0):
$router = new Routing(...); $errorMiddleware = new ErrorMiddleware($responseFactory, $streamFactory, $debugMode); $app = new MiddlewareHandler($errorMiddleware, $router); $response = $app->handle($request); // Middleware catches errors
Action Required: Wrap the router with ErrorMiddleware or create your own error handler to catch exceptions.
Quick Start
use ElliePHP\Components\Routing\Router; use Nyholm\Psr7\ServerRequest; // Configure the router Router::configure([ 'debug_mode' => true, ]); // Define routes Router::get('/', function() { return ['message' => 'Hello World']; }); // Handle request $request = new ServerRequest('GET', '/'); $response = Router::handle($request); echo $response->getBody(); // {"message":"Hello World"}
Booting via Facade
When using the Router facade, the error handling middleware is automatically configured and wrapped around the router. You simply call Router::handle($request):
use ElliePHP\Components\Routing\Router; use Nyholm\Psr7\ServerRequest; // 1. Configure Router::configure(['debug_mode' => true]); // 2. Define Routes Router::get('/', fn() => ['status' => 'ok']); // 3. Handle Request (Middleware is automatically applied) $request = new ServerRequest('GET', '/'); $response = Router::handle($request);
Basic Usage
Defining Routes
// Simple routes Router::get('/users', function() { return ['users' => []]; }); Router::post('/users', function($request) { return ['created' => true]; }); // All HTTP methods Router::get('/users', [UserController::class, 'index']); Router::post('/users', [UserController::class, 'store']); Router::put('/users/{id}', [UserController::class, 'update']); Router::patch('/users/{id}', [UserController::class, 'patch']); Router::delete('/users/{id}', [UserController::class, 'destroy']);
Route Parameters
// Single parameter Router::get('/users/{id}', function($request, $params) { return ['user_id' => $params['id']]; }); // Multiple parameters Router::get('/users/{userId}/posts/{postId}', function($request, $params) { return [ 'user_id' => $params['userId'], 'post_id' => $params['postId'] ]; });
Controllers
namespace App\Controllers; use Psr\Http\Message\ServerRequestInterface; class UserController { public function index(ServerRequestInterface $request): array { return ['users' => []]; } public function show(ServerRequestInterface $request, string $id): array { return ['user' => ['id' => $id]]; } public function store(ServerRequestInterface $request): array { $body = json_decode((string)$request->getBody(), true); return ['message' => 'User created', 'user' => $body]; } } // Register controller routes Router::get('/users', [UserController::class, 'index']); Router::get('/users/{id}', [UserController::class, 'show']); Router::post('/users', [UserController::class, 'store']);
Fluent API (Method Chaining)
The router supports method chaining for more readable configuration.
Single Routes
// Fluent syntax Router::get('/users', [UserController::class, 'index']) ->middleware([AuthMiddleware::class]) ->name('users.index') ->domain('api.example.com'); // Equivalent array syntax (still supported) Router::get('/users', [UserController::class, 'index'], [ 'middleware' => [AuthMiddleware::class], 'name' => 'users.index', 'domain' => 'api.example.com' ]);
Route Groups
// Start with any configuration method Router::prefix('/api/v1') ->middleware([ApiMiddleware::class]) ->domain('api.example.com') ->group(function() { Router::get('/users', [UserController::class, 'index']); Router::post('/users', [UserController::class, 'store']); }); // Nested groups Router::prefix('/api') ->group(function() { Router::prefix('/v1') ->middleware([ApiMiddleware::class]) ->group(function() { Router::get('/users', [UserController::class, 'index']); }); });
Route Groups
// Basic groups Router::group(['prefix' => '/api'], function() { Router::get('/users', [UserController::class, 'index']); Router::post('/users', [UserController::class, 'store']); }); // Routes: /api/users // Groups with middleware Router::group(['middleware' => [AuthMiddleware::class]], function() { Router::get('/profile', [ProfileController::class, 'show']); Router::put('/profile', [ProfileController::class, 'update']); }); // Groups with names Router::group(['name' => 'api'], function() { Router::group(['name' => 'users'], function() { Router::get('/', [UserController::class, 'index'], ['name' => 'index']); // Full name: api.users.index }); });
Domain Routing
Basic Domain Constraints
// Main website routes Router::get('/', function() { return ['message' => 'Welcome']; }, ['domain' => 'example.com']); // API subdomain routes Router::get('/users', [UserController::class, 'index'], [ 'domain' => 'api.example.com' ]); // Admin subdomain routes Router::get('/dashboard', [AdminController::class, 'dashboard'], [ 'domain' => 'admin.example.com' ]);
Domain Groups
// API subdomain with all endpoints Router::group(['domain' => 'api.example.com'], function() { Router::get('/users', [UserController::class, 'index']); Router::post('/users', [UserController::class, 'store']); }); // Admin panel with authentication Router::group([ 'domain' => 'admin.example.com', 'middleware' => [AuthMiddleware::class, AdminMiddleware::class] ], function() { Router::get('/dashboard', [AdminController::class, 'dashboard']); Router::get('/users', [AdminController::class, 'users']); });
Domain Parameters (Multi-Tenant)
// Basic tenant routing Router::get('/dashboard', function($request, $params) { $tenant = $params['tenant']; return ['tenant' => $tenant, 'message' => 'Welcome']; }, ['domain' => '{tenant}.example.com']); // Combine domain and path parameters Router::get('/users/{id}', function($request, $params) { return [ 'tenant' => $params['tenant'], 'user_id' => $params['id'] ]; }, ['domain' => '{tenant}.example.com']);
Multi-Tenant Application
Router::configure([ 'enforce_domain' => true, 'allowed_domains' => [ 'myapp.com', 'app.myapp.com', '{tenant}.myapp.com', ], ]); // Main marketing site Router::group(['domain' => 'myapp.com'], function() { Router::get('/', [MarketingController::class, 'home']); }); // Tenant application routes Router::group(['domain' => '{tenant}.myapp.com'], function() { Router::get('/login', [AuthController::class, 'showLogin']); Router::group(['middleware' => [AuthMiddleware::class]], function() { Router::get('/dashboard', function($request, $params) { return ['tenant' => $params['tenant']]; }); }); });
Middleware
Creating Middleware
namespace App\Middleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; class AuthMiddleware implements MiddlewareInterface { public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ): ResponseInterface { $token = $request->getHeaderLine('Authorization'); if (!$this->isValidToken($token)) { throw new UnauthorizedException('Invalid token'); } $response = $handler->handle($request); return $response->withHeader('X-Authenticated', 'true'); } }
Applying Middleware
// Single middleware Router::get('/protected', [SecureController::class, 'index'], [ 'middleware' => [AuthMiddleware::class] ]); // Multiple middleware (executed in order) Router::get('/admin', [AdminController::class, 'index'], [ 'middleware' => [ AuthMiddleware::class, AdminMiddleware::class, RateLimitMiddleware::class ] ]); // Group middleware Router::group(['middleware' => [AuthMiddleware::class]], function() { Router::get('/profile', [ProfileController::class, 'show']); });
Global Middleware
Router::configure([ 'global_middleware' => [ RequestIdMiddleware::class, LoggingMiddleware::class, CorsMiddleware::class, ], ]); // All routes automatically have global middleware applied Router::get('/users', [UserController::class, 'index']);
Dependency Injection (PSR-11)
// Configure router with PSR-11 container Router::configure([ 'container' => $container, ]); // Controller with dependencies class UserController { public function __construct( private UserRepository $userRepository ) {} public function show(ServerRequestInterface $request, array $params): array { $user = $this->userRepository->findById((int) $params['id']); return ['user' => $user]; } } // Register in container $container->set(UserRepository::class, fn() => new UserRepository()); $container->set(UserController::class, fn($c) => new UserController($c->get(UserRepository::class)) ); // Router will resolve from container Router::get('/users/{id}', [UserController::class, 'show']);
Configuration
Router::configure([ // Directory containing route files 'routes_directory' => __DIR__ . '/routes', // Enable debug mode for detailed errors 'debug_mode' => $_ENV['APP_DEBUG'] ?? false, // Enable route caching for production 'cache_enabled' => $_ENV['APP_ENV'] === 'production', // Cache directory 'cache_directory' => __DIR__ . '/storage/cache', // Enforce domain whitelist 'enforce_domain' => false, // Allowed domains (supports parameters) 'allowed_domains' => [ 'example.com', 'api.example.com', '{tenant}.example.com' ], // Global middleware for all routes 'global_middleware' => [ RequestIdMiddleware::class, LoggingMiddleware::class, ], // PSR-11 container for dependency injection 'container' => $container, ]);
Performance and Caching
Enable caching for production:
Router::configure([ 'cache_enabled' => true, 'cache_directory' => __DIR__ . '/storage/cache', ]); // Clear cache after deploying new routes Router::clearCache();
Performance improvements with caching:
- Route loading: 50-100x faster
- Dispatcher build: 40-80x faster
- Reflection operations: 50x faster
- Domain matching: 20x faster
Debug Features
// Enable debug mode Router::configure(['debug_mode' => true]); // Print formatted route table echo Router::printRoutes(); // Get routes as array $routes = Router::getFormattedRoutes(); // Check configuration if (Router::isDebugMode()) { echo "Debug mode is enabled\n"; }
Complete Example
require 'vendor/autoload.php'; use ElliePHP\Components\Routing\Router; use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7Server\ServerRequestCreator; // Configure router Router::configure([ 'debug_mode' => $_ENV['APP_DEBUG'] ?? false, 'cache_enabled' => $_ENV['APP_ENV'] === 'production', 'cache_directory' => __DIR__ . '/storage/cache', ]); // Define routes Router::get('/', function() { return ['message' => 'Welcome to the API']; }); Router::group(['prefix' => '/api/v1'], function() { Router::post('/auth/login', [AuthController::class, 'login']); Router::group(['middleware' => [AuthMiddleware::class]], function() { Router::get('/profile', [ProfileController::class, 'show']); Router::group([ 'prefix' => '/admin', 'middleware' => [AdminMiddleware::class] ], function() { Router::get('/users', [AdminController::class, 'users']); }); }); }); // Create PSR-7 request and handle $psr17Factory = new Psr17Factory(); $creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory); $request = $creator->fromGlobals(); $response = Router::handle($request); // Send response http_response_code($response->getStatusCode()); foreach ($response->getHeaders() as $name => $values) { foreach ($values as $value) { header("$name: $value", false); } } echo $response->getBody();
Requirements
- PHP 8.4 or higher
- psr/http-server-middleware ^1.0
- psr/http-server-handler ^1.0
- nyholm/psr7 ^1.8
License
MIT License