Skip to content

A minimal, fast routing component for ElliePHP API framework based on FastRoute and PSR-7/PSR-15 standards

License

Notifications You must be signed in to change notification settings

ElliePHP/Routing

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ElliePHP Routing Component

Latest Version PHP Version License Total Downloads

A minimal, fast routing component for ElliePHP API framework based on FastRoute and PSR-7/PSR-15 standards.

Features

  • Fast Routing: Built on nikic/fast-route for optimal performance
  • Performance Optimized: Multiple caching layers and optimizations for high-traffic applications
    • Dispatcher caching per domain
    • Reflection metadata caching for controller methods
    • Route hash-based cache invalidation
    • Domain regex pattern caching
    • Cache age validation (< 5 seconds = trusted cache)
  • PSR Standards: Full PSR-7 (HTTP messages) and PSR-15 (middleware) compliance
  • Flexible Handlers: Support for closures, controller classes, and callable arrays
  • Middleware Support: PSR-15 middleware with proper stack execution
  • Route Groups: Organize routes with shared prefixes, middleware, and names
  • Domain Routing: Support for subdomain and multi-tenant routing with domain parameters
  • Route Caching: Production-ready route caching for improved performance
  • Debug Mode: Detailed error messages, timing info, and route visualization
  • Type Safe: PHP 8.4+ with strict types and proper type hints

Installation

composer require elliephp/routing

Quick Start

Basic Setup

<?php

require 'vendor/autoload.php';

use ElliePHP\Components\Routing\Router;
use Nyholm\Psr7\ServerRequest;

// Configure the router (optional)
Router::configure([
    'debug_mode' => true,
]);

// Define a simple route
Router::get('/', function() {
    return ['message' => 'Hello World'];
});

// Handle the incoming request
$request = new ServerRequest('GET', '/');
$response = Router::handle($request);

// Output: {"message":"Hello World"}
echo $response->getBody();

Using Without Facade

If you prefer not to use the static facade, you can work directly with the Routing class:

<?php

use ElliePHP\Components\Routing\Core\Routing;
use Nyholm\Psr7\ServerRequest;

// Create router instance
$router = new Routing(
    routes_directory: __DIR__ . '/routes',
    debugMode: true,
    cacheEnabled: false
);

// Define routes
$router->get('/', function() {
    return ['message' => 'Hello World'];
});

$router->get('/users/{id}', function($request, $params) {
    return ['user_id' => $params['id']];
});

// Handle request
$request = new ServerRequest('GET', '/users/42');
$response = $router->handle($request);

Usage Guide

Defining Routes

Simple Routes

// Using the facade
Router::get('/users', function() {
    return ['users' => []];
});

Router::post('/users', function($request) {
    return ['created' => true];
});

// Without facade
$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']
    ];
});

// Optional parameters with controller
Router::get('/search/{query}', [SearchController::class, 'search']);

Controllers

Basic Controller

namespace App\Controllers;

use Psr\Http\Message\ServerRequestInterface;

class UserController
{
    public function index(ServerRequestInterface $request): array
    {
        // Return array for automatic JSON response
        return [
            'users' => [
                ['id' => 1, 'name' => 'Alice'],
                ['id' => 2, 'name' => 'Bob'],
            ]
        ];
    }
    
    public function show(ServerRequestInterface $request, string $id): array
    {
        // Route parameters are automatically injected
        return [
            'user' => [
                'id' => $id,
                'name' => 'User ' . $id
            ]
        ];
    }
    
    public function store(ServerRequestInterface $request): array
    {
        // Access request body
        $body = json_decode((string)$request->getBody(), true);
        
        return [
            'message' => 'User created',
            'user' => $body
        ];
    }
}

Registering Controller Routes

// Array syntax
Router::get('/users', [UserController::class, 'index']);
Router::get('/users/{id}', [UserController::class, 'show']);

// String syntax (alternative)
Router::get('/users', 'UserController@index');

// With options
Router::post('/users', [UserController::class, 'store'], [
    'middleware' => [AuthMiddleware::class],
    'name' => 'users.store'
]);

Returning PSR-7 Responses

use Psr\Http\Message\ResponseInterface;
use Nyholm\Psr7\Response;

class UserController
{
    public function custom(ServerRequestInterface $request): ResponseInterface
    {
        // Build custom PSR-7 response
        return new Response(
            status: 201,
            headers: ['Content-Type' => 'application/json'],
            body: json_encode(['created' => true])
        );
    }
}

Route Groups

Basic Groups

// With facade
Router::group(['prefix' => '/api'], function() {
    Router::get('/users', [UserController::class, 'index']);
    Router::post('/users', [UserController::class, 'store']);
});
// Routes: /api/users

// Without facade
$router->group(['prefix' => '/api'], function($router) {
    $router->get('/users', [UserController::class, 'index']);
});

Nested Groups

Router::group(['prefix' => '/api'], function() {
    Router::group(['prefix' => '/v1'], function() {
        Router::get('/users', [UserController::class, 'index']);
        // Route: /api/v1/users
        
        Router::group(['prefix' => '/admin'], function() {
            Router::get('/dashboard', [AdminController::class, 'dashboard']);
            // Route: /api/v1/admin/dashboard
        });
    });
});

Groups with Middleware

Router::group(['middleware' => [AuthMiddleware::class]], function() {
    Router::get('/profile', [ProfileController::class, 'show']);
    Router::put('/profile', [ProfileController::class, 'update']);
});

// Nested groups inherit parent middleware
Router::group(['middleware' => [AuthMiddleware::class]], function() {
    Router::group(['middleware' => [AdminMiddleware::class]], function() {
        Router::get('/admin/users', [AdminController::class, 'users']);
        // Has both AuthMiddleware and AdminMiddleware
    });
});

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

Domain routing allows you to create routes that only respond to specific domains or subdomains. This is perfect for multi-tenant applications, API subdomains, or separating admin panels.

Basic Domain Constraints

// Main website routes
Router::get('/', function() {
    return ['message' => 'Welcome to example.com'];
}, ['domain' => 'example.com']);

Router::get('/about', function() {
    return ['page' => 'about'];
}, ['domain' => 'example.com']);

// API subdomain routes
Router::get('/users', [UserController::class, 'index'], [
    'domain' => 'api.example.com'
]);

Router::post('/users', [UserController::class, 'store'], [
    'domain' => 'api.example.com'
]);

// Admin subdomain routes
Router::get('/dashboard', [AdminController::class, 'dashboard'], [
    'domain' => 'admin.example.com'
]);

Router::get('/users', [AdminController::class, 'users'], [
    'domain' => 'admin.example.com'
]);

Domain Groups

Group multiple routes under the same domain to keep your code organized:

// API subdomain with all endpoints
Router::group(['domain' => 'api.example.com'], function() {
    Router::get('/users', [UserController::class, 'index']);
    Router::post('/users', [UserController::class, 'store']);
    Router::get('/posts', [PostController::class, 'index']);
    Router::get('/comments', [CommentController::class, 'index']);
});

// API with versioning
Router::group(['domain' => 'api.example.com', 'prefix' => '/v1'], function() {
    Router::get('/users', [UserController::class, 'index']);
    Router::get('/posts', [PostController::class, 'index']);
    // Accessible at: http://api.example.com/v1/users
});

Router::group(['domain' => 'api.example.com', 'prefix' => '/v2'], function() {
    Router::get('/users', [UserControllerV2::class, 'index']);
    Router::get('/posts', [PostControllerV2::class, 'index']);
    // Accessible at: http://api.example.com/v2/users
});

// 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']);
    Router::get('/settings', [AdminController::class, 'settings']);
    Router::get('/reports', [AdminController::class, 'reports']);
});

Domain Parameters (Multi-Tenant SaaS)

Extract subdomain parts as parameters for multi-tenant applications:

// Basic tenant routing
Router::get('/dashboard', function($request, $params) {
    $tenant = $params['tenant'];
    
    // Load tenant-specific data
    $tenantData = Database::getTenant($tenant);
    
    return [
        'tenant' => $tenant,
        'company' => $tenantData['company_name'],
        'message' => 'Welcome to your dashboard'
    ];
}, ['domain' => '{tenant}.example.com']);

// Access: http://acme.example.com/dashboard
// Returns: {"tenant":"acme","company":"Acme Corp","message":"Welcome to your dashboard"}

// Access: http://widgets.example.com/dashboard
// Returns: {"tenant":"widgets","company":"Widgets Inc","message":"Welcome to your dashboard"}

// Combine domain and path parameters
Router::get('/users/{id}', function($request, $params) {
    $tenant = $params['tenant'];
    $userId = $params['id'];
    
    // Load user from tenant database
    $user = Database::getTenantUser($tenant, $userId);
    
    return [
        'tenant' => $tenant,
        'user' => $user
    ];
}, ['domain' => '{tenant}.example.com']);

// Access: http://acme.example.com/users/42
// Returns: {"tenant":"acme","user":{"id":42,"name":"John Doe"}}

// Real-world example: Tenant-specific API
Router::get('/api/projects', function($request, $params) {
    $tenant = $params['tenant'];
    return [
        'tenant' => $tenant,
        'projects' => ProjectService::getForTenant($tenant)
    ];
}, ['domain' => '{tenant}.example.com']);

// Access: http://acme.example.com/api/projects
// Access: http://widgets.example.com/api/projects

Multi-Tenant Application Example

Complete multi-tenant SaaS application structure:

// Configure domain enforcement
Router::configure([
    'enforce_domain' => true,
    'allowed_domains' => [
        'myapp.com',              // Main marketing site
        'app.myapp.com',          // Main app domain
        '{tenant}.myapp.com',     // Tenant subdomains
    ],
]);

// Main marketing site
Router::group(['domain' => 'myapp.com'], function() {
    Router::get('/', [MarketingController::class, 'home']);
    Router::get('/pricing', [MarketingController::class, 'pricing']);
    Router::get('/signup', [MarketingController::class, 'signup']);
});

// Tenant application routes
Router::group(['domain' => '{tenant}.myapp.com'], function() {
    // Public routes
    Router::get('/login', [AuthController::class, 'showLogin']);
    Router::post('/login', [AuthController::class, 'login']);
    
    // Protected tenant routes
    Router::group(['middleware' => [AuthMiddleware::class]], function() {
        Router::get('/dashboard', function($request, $params) {
            $tenant = $params['tenant'];
            return [
                'tenant' => $tenant,
                'stats' => DashboardService::getStats($tenant)
            ];
        });
        
        Router::get('/projects', [ProjectController::class, 'index']);
        Router::post('/projects', [ProjectController::class, 'store']);
        Router::get('/projects/{id}', [ProjectController::class, 'show']);
        
        Router::get('/team', [TeamController::class, 'index']);
        Router::post('/team/invite', [TeamController::class, 'invite']);
        
        Router::get('/settings', [SettingsController::class, 'show']);
        Router::put('/settings', [SettingsController::class, 'update']);
    });
});

// Examples:
// http://myapp.com/ - Marketing site
// http://acme.myapp.com/dashboard - Acme's dashboard
// http://widgets.myapp.com/projects - Widgets Inc's projects
// http://startup.myapp.com/team - Startup's team page

Multiple Domain Parameters

Extract multiple parts from the domain for advanced routing:

// Regional routing
Router::get('/api/data', function($request, $params) {
    $region = $params['region'];
    $service = $params['service'];
    
    return [
        'service' => $service,
        'region' => $region,
        'endpoint' => "https://{$service}.{$region}.example.com",
        'data' => RegionalService::getData($region, $service)
    ];
}, ['domain' => '{service}.{region}.example.com']);

// Access: http://api.us-east.example.com/api/data
// Returns: {"service":"api","region":"us-east","endpoint":"https://api.us-east.example.com","data":[...]}

// Access: http://cdn.eu-west.example.com/api/data
// Returns: {"service":"cdn","region":"eu-west","endpoint":"https://cdn.eu-west.example.com","data":[...]}

// Multi-tenant with environment
Router::get('/status', function($request, $params) {
    return [
        'tenant' => $params['tenant'],
        'environment' => $params['env'],
        'status' => 'operational'
    ];
}, ['domain' => '{tenant}.{env}.example.com']);

// Access: http://acme.staging.example.com/status
// Returns: {"tenant":"acme","environment":"staging","status":"operational"}

// Access: http://acme.production.example.com/status
// Returns: {"tenant":"acme","environment":"production","status":"operational"}

Domain Configuration

Router::configure([
    // Enforce domain whitelist (reject unlisted domains with 403)
    'enforce_domain' => true,
    
    // Allowed domains (supports patterns with parameters)
    'allowed_domains' => [
        'example.com',
        'api.example.com',
        'admin.example.com',
        '{tenant}.example.com',
        '{app}.{region}.example.com'
    ],
]);

Routes Without Domain Constraints

Routes without domain constraints work on any domain:

// Health check endpoint - works on all domains
Router::get('/health', function() {
    return ['status' => 'ok', 'timestamp' => time()];
});

// Metrics endpoint - accessible from any domain
Router::get('/metrics', function() {
    return [
        'requests' => MetricsService::getRequestCount(),
        'uptime' => MetricsService::getUptime()
    ];
});

// This route works on:
// - http://example.com/health
// - http://api.example.com/health
// - http://admin.example.com/health
// - http://tenant1.example.com/health
// - http://any-subdomain.example.com/health

Real-World Complete Example

<?php

use ElliePHP\Components\Routing\Router;

// Configure domains
Router::configure([
    'enforce_domain' => true,
    'allowed_domains' => [
        'myapp.com',
        'api.myapp.com',
        'admin.myapp.com',
        '{tenant}.myapp.com'
    ],
]);

// Marketing site (myapp.com)
Router::group(['domain' => 'myapp.com'], function() {
    Router::get('/', [HomeController::class, 'index']);
    Router::get('/features', [HomeController::class, 'features']);
    Router::get('/pricing', [HomeController::class, 'pricing']);
    Router::post('/signup', [SignupController::class, 'register']);
});

// Public API (api.myapp.com)
Router::group(['domain' => 'api.myapp.com', 'prefix' => '/v1'], function() {
    // Public endpoints
    Router::post('/auth/login', [ApiAuthController::class, 'login']);
    Router::post('/auth/register', [ApiAuthController::class, 'register']);
    
    // Protected API endpoints
    Router::group(['middleware' => [ApiAuthMiddleware::class]], function() {
        Router::get('/users', [ApiUserController::class, 'index']);
        Router::get('/users/{id}', [ApiUserController::class, 'show']);
        Router::post('/users', [ApiUserController::class, 'store']);
    });
});

// Admin panel (admin.myapp.com)
Router::group([
    'domain' => 'admin.myapp.com',
    'middleware' => [AuthMiddleware::class, AdminMiddleware::class]
], function() {
    Router::get('/dashboard', [AdminDashboardController::class, 'index']);
    Router::get('/tenants', [AdminTenantController::class, 'index']);
    Router::get('/tenants/{id}', [AdminTenantController::class, 'show']);
    Router::post('/tenants', [AdminTenantController::class, 'create']);
    Router::delete('/tenants/{id}', [AdminTenantController::class, 'delete']);
});

// Multi-tenant application ({tenant}.myapp.com)
Router::group(['domain' => '{tenant}.myapp.com'], function() {
    // Public tenant pages
    Router::get('/login', [TenantAuthController::class, 'showLogin']);
    Router::post('/login', [TenantAuthController::class, 'login']);
    
    // Protected tenant routes
    Router::group(['middleware' => [TenantAuthMiddleware::class]], function() {
        Router::get('/dashboard', function($request, $params) {
            $tenant = $params['tenant'];
            return view('dashboard', [
                'tenant' => TenantService::load($tenant),
                'stats' => DashboardService::getStats($tenant)
            ]);
        });
        
        Router::get('/projects', [TenantProjectController::class, 'index']);
        Router::post('/projects', [TenantProjectController::class, 'store']);
        Router::get('/projects/{id}', [TenantProjectController::class, 'show']);
        Router::put('/projects/{id}', [TenantProjectController::class, 'update']);
        Router::delete('/projects/{id}', [TenantProjectController::class, 'destroy']);
    });
});

// Health check - works on all domains
Router::get('/health', function() {
    return ['status' => 'ok'];
});

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 {
        // Check authentication before handling request
        $token = $request->getHeaderLine('Authorization');
        
        if (!$this->isValidToken($token)) {
            throw new UnauthorizedException('Invalid token');
        }
        
        // Continue to next middleware or handler
        $response = $handler->handle($request);
        
        // Optionally modify response
        return $response->withHeader('X-Authenticated', 'true');
    }
    
    private function isValidToken(string $token): bool
    {
        // Your authentication logic
        return !empty($token);
    }
}

Applying Middleware

// Single middleware on route
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']);
    Router::put('/profile', [ProfileController::class, 'update']);
});

Closure Middleware

Router::get('/custom', [CustomController::class, 'index'], [
    'middleware' => [
        function($request, $next) {
            // Before handler
            $start = microtime(true);
            
            // Process request
            $response = $next($request);
            
            // After handler
            $duration = microtime(true) - $start;
            return $response->withHeader('X-Response-Time', $duration . 's');
        }
    ]
]);

Middleware Execution Order

Router::get('/test', $handler, [
    'middleware' => [
        FirstMiddleware::class,   // Executes first (before)
        SecondMiddleware::class,  // Executes second (before)
        ThirdMiddleware::class,   // Executes third (before)
        // Handler executes here
        // ThirdMiddleware (after)
        // SecondMiddleware (after)
        // FirstMiddleware (after)
    ]
]);

Configuration

Development Configuration

Router::configure([
    'debug_mode' => true,
    'cache_enabled' => false,
]);

// View all registered routes
echo Router::printRoutes();

Production Configuration

Router::configure([
    'routes_directory' => __DIR__ . '/routes',
    'cache_enabled' => true,
    'cache_directory' => __DIR__ . '/storage/cache',
    'debug_mode' => false,
]);

// Clear cache when deploying new routes
Router::clearCache();

Configuration Options

Router::configure([
    // Directory containing route files (default: '/')
    'routes_directory' => __DIR__ . '/routes',
    
    // Enable debug mode for detailed errors (default: false)
    'debug_mode' => $_ENV['APP_DEBUG'] ?? false,
    
    // Enable route caching for production (default: false)
    'cache_enabled' => $_ENV['APP_ENV'] === 'production',
    
    // Cache directory (default: sys_get_temp_dir())
    'cache_directory' => __DIR__ . '/storage/cache',
    
    // Custom error formatter (default: JsonErrorFormatter)
    'error_formatter' => new HtmlErrorFormatter(),
    
    // Enforce domain whitelist (default: false)
    'enforce_domain' => false,
    
    // Allowed domains (supports domain parameters like {tenant}.example.com)
    'allowed_domains' => [
        'example.com',
        'api.example.com',
        '{tenant}.example.com'
    ],
]);

Custom Error Formatters

use ElliePHP\Components\Routing\Core\HtmlErrorFormatter;
use ElliePHP\Components\Routing\Core\JsonErrorFormatter;

// Use HTML error pages
Router::configure([
    'error_formatter' => new HtmlErrorFormatter(),
]);

// Use JSON errors (default)
Router::configure([
    'error_formatter' => new JsonErrorFormatter(),
]);

// Create custom formatter
class CustomErrorFormatter implements ErrorFormatterInterface
{
    public function format(Throwable $e, bool $debugMode): array
    {
        return [
            'error' => $e->getMessage(),
            'code' => $e->getCode(),
        ];
    }
}

Route Files

Organize routes in separate files:

// routes/api.php
<?php

use ElliePHP\Components\Routing\Router;

Router::group(['prefix' => '/api/v1'], function() {
    require __DIR__ . '/api/users.php';
    require __DIR__ . '/api/posts.php';
});
// routes/api/users.php
<?php

use ElliePHP\Components\Routing\Router;

Router::get('/users', [UserController::class, 'index']);
Router::get('/users/{id}', [UserController::class, 'show']);
Router::post('/users', [UserController::class, 'store']);
Router::put('/users/{id}', [UserController::class, 'update']);
Router::delete('/users/{id}', [UserController::class, 'destroy']);

Debug Features

Route Listing

// Print formatted route table
echo Router::printRoutes();

/* Output:
====================================================================================================
METHOD   PATH                                     NAME                           HANDLER
====================================================================================================
GET      /users                                   get.users                      UserController@index
GET      /users/{id}                              get.users.id                   UserController@show
POST     /users                                   post.users                     UserController@store
====================================================================================================
Total routes: 3
*/

// Get routes as array
$routes = Router::getFormattedRoutes();

Debug Headers

When debug mode is enabled, responses automatically include:

X-Debug-Time: 4.23ms
X-Debug-Routes: 15

Detailed Error Messages

Debug mode provides comprehensive error information:

{
  "error": "Route not found",
  "status": 404,
  "debug": {
    "exception": "ElliePHP\\Components\\Routing\\Exceptions\\RouteNotFoundException",
    "file": "/path/to/Routing.php",
    "line": 246,
    "trace": "..."
  }
}

Route Inspection

// Check configuration
if (Router::isDebugMode()) {
    echo "Debug mode is enabled\n";
}

if (Router::isCacheEnabled()) {
    echo "Cache is enabled\n";
}

// Get all routes
$routes = Router::getRoutes();
foreach ($routes as $route) {
    echo "{$route['method']} {$route['path']}\n";
}

Performance & Caching

Performance Optimizations

ElliePHP Routing includes multiple performance optimizations designed for high-traffic production environments:

1. Route Caching

Routes are serialized and cached to avoid recompilation on every request:

Router::configure([
    'cache_enabled' => true,
    'cache_directory' => __DIR__ . '/storage/cache',
]);

Performance Impact: Eliminates route file loading and parsing overhead on subsequent requests.

2. Dispatcher Caching

FastRoute dispatchers are cached per domain, avoiding rebuilding on every request:

// First request to example.com - builds and caches dispatcher
// Subsequent requests to example.com - reuses cached dispatcher
// First request to api.example.com - builds separate cached dispatcher

Performance Impact: Reduces dispatcher compilation time by ~90% on subsequent requests.

3. Reflection Metadata Caching

Controller method parameter metadata is extracted once and cached:

class UserController {
    // Reflection metadata cached after first invocation
    public function show(ServerRequestInterface $request, string $id): array {
        return ['user_id' => $id];
    }
}

Performance Impact: Eliminates expensive reflection operations on every request. Direct method invocation is used instead of ReflectionMethod::invokeArgs().

4. Domain Regex Caching

Domain patterns are compiled to regex once and cached:

Router::get('/dashboard', $handler, [
    'domain' => '{tenant}.example.com'  // Compiled once, cached forever
]);

Performance Impact: Avoids regex compilation overhead on every domain match.

5. Smart Cache Validation

Cache validation uses a 5-second trust window to avoid expensive filesystem checks:

// Cache age < 5 seconds: Trusted without validation
// Cache age >= 5 seconds: Validates against route file modification times

Performance Impact: Reduces filesystem I/O by ~95% in high-traffic scenarios.

6. Route Hash Invalidation

Uses CRC32 hashing for efficient cache invalidation:

// Hash calculated only when routes change
// Lightweight comparison instead of deep route comparison

Performance Impact: Fast cache validation with minimal CPU overhead.

Enable Caching

Router::configure([
    'cache_enabled' => true,
    'cache_directory' => __DIR__ . '/storage/cache',
]);

Clear Cache

// Clear cache manually
Router::clearCache();

// Or delete the cache file
unlink(__DIR__ . '/storage/cache/ellie_routes.cache');

Cache Behavior

  • Cache is automatically disabled when debug_mode is true
  • Routes are cached after first load
  • Cache is loaded on subsequent requests
  • Failed cache loads fall back to loading routes normally
  • Cache validation skipped for requests within 5 seconds of last validation

Performance Benchmarks

Typical performance improvements with caching enabled:

Metric Without Cache With Cache Improvement
Route Loading ~5-10ms ~0.1ms 50-100x faster
Dispatcher Build ~2-4ms ~0.05ms 40-80x faster
Reflection Operations ~0.5ms per call ~0.01ms per call 50x faster
Domain Matching ~0.2ms per pattern ~0.01ms per pattern 20x faster

Production Recommendations

For optimal performance in production:

Router::configure([
    // Enable caching
    'cache_enabled' => true,
    'cache_directory' => __DIR__ . '/storage/cache',
    
    // Disable debug mode
    'debug_mode' => false,
    
    // Use OPcache for PHP bytecode caching
    // php.ini: opcache.enable=1
    
    // Preload routes on deployment
    // Clear cache after deploying new routes
]);

// Warm up cache after deployment
Router::clearCache();
$request = new ServerRequest('GET', '/');
Router::handle($request); // Builds and caches routes

Memory Usage

The router is designed for minimal memory footprint:

  • Only essential data stored in cache structures
  • Closures and non-serializable data excluded from cache
  • Dispatcher cache stores only dispatcher instance and hash
  • Reflection cache stores only parameter metadata arrays

Typical Memory Usage: ~50-200KB for 100 routes (depending on complexity)

Testing

Basic Testing

use ElliePHP\Components\Routing\Router;
use Nyholm\Psr7\ServerRequest;
use PHPUnit\Framework\TestCase;

class RouteTest extends TestCase
{
    protected function setUp(): void
    {
        // Reset router state between tests
        Router::resetInstance();
        Router::reset();
    }
    
    public function testUserRoute(): void
    {
        Router::get('/users/{id}', function($request, $params) {
            return ['user_id' => $params['id']];
        });
        
        $request = new ServerRequest('GET', '/users/123');
        $response = Router::handle($request);
        
        $this->assertEquals(200, $response->getStatusCode());
        
        $body = json_decode((string)$response->getBody(), true);
        $this->assertEquals('123', $body['user_id']);
    }
}

Testing with Controllers

public function testUserController(): void
{
    Router::get('/users', [UserController::class, 'index']);
    
    $request = new ServerRequest('GET', '/users');
    $response = Router::handle($request);
    
    $this->assertEquals(200, $response->getStatusCode());
    $this->assertStringContainsString('users', (string)$response->getBody());
}

Testing Middleware

public function testMiddleware(): void
{
    Router::get('/protected', function() {
        return ['protected' => true];
    }, [
        'middleware' => [TestMiddleware::class]
    ]);
    
    $request = new ServerRequest('GET', '/protected');
    $response = Router::handle($request);
    
    $this->assertTrue($response->hasHeader('X-Test-Middleware'));
}

Advanced Usage

Programmatic Route Registration

Router::registerRoutes([
    [
        'method' => 'GET',
        'path' => '/users',
        'class' => UserController::class,
        'handler' => 'index',
        'middleware' => [AuthMiddleware::class],
        'name' => 'users.index'
    ],
    [
        'method' => 'POST',
        'path' => '/users',
        'class' => UserController::class,
        'handler' => 'store',
        'middleware' => [AuthMiddleware::class],
        'name' => 'users.store'
    ],
]);

Named Routes

Router::get('/users/{id}', [UserController::class, 'show'], [
    'name' => 'users.show'
]);

Router::post('/users', [UserController::class, 'store'], [
    'name' => 'users.store'
]);

// Access route names
$routes = Router::getRoutes();
foreach ($routes as $route) {
    echo "Route: {$route['name']}\n";
}

Custom Route Names

// Automatic naming: get.users.id
Router::get('/users/{id}', [UserController::class, 'show']);

// Custom naming
Router::get('/users/{id}', [UserController::class, 'show'], [
    'name' => 'user.profile'
]);

Complete Example

<?php

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() {
    // Public routes
    Router::post('/auth/login', [AuthController::class, 'login']);
    Router::post('/auth/register', [AuthController::class, 'register']);
    
    // Protected routes
    Router::group(['middleware' => [AuthMiddleware::class]], function() {
        Router::get('/profile', [ProfileController::class, 'show']);
        Router::put('/profile', [ProfileController::class, 'update']);
        
        // Admin routes
        Router::group([
            'prefix' => '/admin',
            'middleware' => [AdminMiddleware::class]
        ], function() {
            Router::get('/users', [AdminController::class, 'users']);
            Router::get('/stats', [AdminController::class, 'stats']);
        });
    });
});

// Create PSR-7 request from globals
$psr17Factory = new Psr17Factory();
$creator = new ServerRequestCreator(
    $psr17Factory,
    $psr17Factory,
    $psr17Factory,
    $psr17Factory
);
$request = $creator->fromGlobals();

// Handle request
$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

Resources

License

MIT License

About

A minimal, fast routing component for ElliePHP API framework based on FastRoute and PSR-7/PSR-15 standards

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Languages