wilaak/radix-router

High-performance radix tree based HTTP request router for PHP.

v3.0.1 2025-08-12 07:50 UTC

This package is auto-updated.

Last update: 2025-08-15 20:15:24 UTC


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.

tree visualiation

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.