wilaak / radix-router
High-performance radix tree based HTTP request router for PHP.
Requires
- php: >=8.0
Requires (Dev)
- php: >=8.3
- phpunit/phpunit: ^11.0
README
High-performance radix tree based HTTP request router for PHP (see benchmarks)
Overview
- High-performance O(k) dynamic route matching, where k is the number of segments in the path.
- Supports parameters, including wildcard and optional segments for flexible route definitions.
- Static routes are stored in a hash map providing fast minimal allocation lookups for exact matches.
How does it work?
The router splits the path into segments and walks the tree, matching each segment in order. Because the tree only branches where routes differ, the router can quickly skip irrelevant routes and find the correct handler with minimal comparisons.
Install
Install with composer:
composer require wilaak/radix-router
Or simply include it in your project:
require '/path/to/RadixRouter.php'
Requires PHP 8.0 or newer. (PHP 8.3 for tests)
Usage Example
Here's a basic usage example using the typical SAPI web environment:
use Wilaak\Http\RadixRouter; // Create a new router instance $router = new RadixRouter(); // Register a route with an optional parameter and a handler $router->add('GET', '/:world?', function ($world = 'World') { echo "Hello, $world!"; }); // Get the HTTP method and path from the request $method = strtoupper( $_SERVER['REQUEST_METHOD'] ); $path = rawurldecode( strtok($_SERVER['REQUEST_URI'], '?') ); // Look up the route for the current request $result = $router->lookup($method, $path); switch ($result['code']) { case 200: // Route matched: call the handler with parameters $result['handler'](...$result['params']); break; case 404: // No matching route found http_response_code(404); echo '404 Not Found'; break; case 405: // Method not allowed for this route header('Allow: ' . implode(', ', $result['allowed_methods'])); http_response_code(405); echo '405 Method Not Allowed'; break; }
Registering Routes
Routes are registered using the add()
method. You can assign any value as the handler. The order of route matching is: static > parameter > wildcard.
Note: Paths are normalized by removing trailing slashes. For example, both
/about
and/about/
will be treated as the same route.
Below is an example showing the different ways to define routes:
// Static route for a single method $router->add('GET', '/about', 'handler'); // Static route for both GET and POST methods $router->add(['GET', 'POST'], '/form', 'handler'); // Required parameter $router->add('GET', '/users/:id', 'handler'); // Example requests: // /users/123 -> matches '/users/:id' (captures "123") // /users -> no-match // Optional parameter (must be in the last trailing segment(s)) $router->add('GET', '/hello/:name?', 'handler'); // Example requests: // /hello -> matches // /hello/alice -> matches (captures "alice") // Multiple trailing optional parameters (must be at the end) $router->add('GET', '/archive/:year?/:month?', 'handler'); // Example requests: // /archive -> matches // /archive/2024 -> matches (captures "2024") // /archive/2024/06 -> matches (captures "2024", "06") // Wildcard parameter (only allowed as last segment) $router->add('GET', '/files/:path*', 'handler'); // Example requests: // /files -> matches (captures "") // /files/readme.txt -> matches (captures "readme.txt") // /files/images/photo.jpg -> matches (captures "images/photo.jpg")
How to Cache Routes
Rebuilding the route tree on every request or application startup can slow down performance.
Note: Anonymous functions (closures) are not supported for route caching because they cannot be serialized. When caching routes, only use handlers that can be safely represented as strings, arrays, or serializable objects.
Note: When implementing route caching, care should be taken to avoid race conditions when rebuilding the cache file. Ensure that the cache is written atomically so that each request can always fully load a valid cache file without errors or partial data.
Here is a simple cache implementation:
$cacheFile = __DIR__ . '/routes.cache.php'; if (!file_exists($cacheFile)) { // Build and register your routes here $router->add('GET', '/', 'handler'); // ...add more routes as needed // Prepare the data to cache $routes = [ 'tree' => $router->tree, 'static' => $router->static, ]; // Export as PHP code for fast loading $export = '<?php return ' . var_export($routes, true) . ';'; // Atomically write cache file $tmpFile = $cacheFile . '.' . uniqid('', true) . '.tmp'; file_put_contents($tmpFile, $export, LOCK_EX); rename($tmpFile, $cacheFile); } // Load cached routes $routes = require $cacheFile; $router->tree = $routes['tree']; $router->static = $routes['static'];
By storing your routes in a PHP file, you let PHP’s OPcache handle the heavy lifting, making startup times nearly instantaneous.
Note on HEAD Requests
According to the HTTP specification, any route that handles a GET request should also support HEAD requests. RadixRouter does not automatically add this behavior. If you are running outside a standard web server environment (such as in a custom server), ensure that your GET routes also respond appropriately to HEAD requests. Responses to HEAD requests must not include a message body.
Benchmarks
Single-threaded benchmark (Xeon E-2136, PHP 8.4.8 cli OPcache enabled):
Simple App (33 Routes)
Router | Register | Lookups | Memory Usage | Peak Memory |
---|---|---|---|---|
RadixRouter | 0.05 ms | 2,977,816/sec | 384 KB | 458 KB |
FastRoute | 1.92 ms | 2,767,883/sec | 429 KB | 1,337 KB |
SymfonyRouter | 6.84 ms | 1,722,432/sec | 573 KB | 1,338 KB |
Avatax API (256 Routes)
Router | Register | Lookups | Memory Usage | Peak Memory |
---|---|---|---|---|
RadixRouter | 0.27 ms | 2,006,929/sec | 688 KB | 690 KB |
FastRoute | 4.94 ms | 707,516/sec | 549 KB | 1,337 KB |
SymfonyRouter | 12.60 ms | 1,182,060/sec | 1,291 KB | 1,587 KB |
Bitbucket API (178 Routes)
Router | Register | Lookups | Memory Usage | Peak Memory |
---|---|---|---|---|
RadixRouter | 0.23 ms | 1,623,718/sec | 641 KB | 643 KB |
FastRoute | 3.81 ms | 371,104/sec | 555 KB | 1,337 KB |
SymfonyRouter | 12.16 ms | 910,064/sec | 1,180 KB | 1,419 KB |
License
This library is licensed under the WTFPL-2.0 license. Do whatever you want with it.