chani/wajha

A fast, lightweight HTTP router for PHP 8.5+ combining O(1) static maps and bucketed PCRE2 regex evaluation.

Maintainers

Package info

github.com/chani/wajha-router

pkg:composer/chani/wajha

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-07-21 12:01 UTC

This package is auto-updated.

Last update: 2026-07-21 21:14:01 UTC


README

A fast, lightweight HTTP router for PHP 8.5+ combining $O(1)$ static hash map lookups with first-character bucketed PCRE2 regex evaluation for dynamic routes.

Technical Features

  • O(1) Static Fast-Path: Direct 2D array lookup ($staticRoutes[$method][$uri]) bypassing the PCRE2 engine entirely.
  • First-Character Path Bucketing: Dynamic routes are partitioned by URI prefix to avoid evaluating unrelated regex chunks.
  • PCRE2 Chunked Matching: Dynamic paths are evaluated in 30-route chunks using native duplicate group names (?J) and (*MARK:N) identifiers directly inside PCRE2.
  • Zero-Stack Dispatching: Inlined execution path without VM function frame call stack overhead during dispatching.
  • Compile-Time Transformations: Type shorthands, enum constraints, and route groups compile down to raw PCRE2 patterns with no runtime overhead.

Requirements

  • PHP 8.5 or higher
  • pcre2 extension enabled

Installation

composer require chani/wajha

Usage Examples

1. Basic Setup & Dispatching

Routes are registered via WajhaCompiler and evaluated via WajhaDispatcher.

use Safi\Wajha\WajhaCompiler;
use Safi\Wajha\WajhaDispatcher;

$compiler = new WajhaCompiler();

$compiler->get('/users', 'UserController@index');
$compiler->post('/users', 'UserController@store');

$compiledData = $compiler->compile();
$dispatcher = new WajhaDispatcher($compiledData);

// HTTP Method MUST be uppercase
$result = $dispatcher->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);

switch ($result[0]) {
    case WajhaDispatcher::FOUND:
        $handler = $result[1];
        $vars = $result[2];
        break;

    case WajhaDispatcher::METHOD_NOT_ALLOWED:
        $allowedMethods = $result[1]; // Array of allowed HTTP methods
        break;

    case WajhaDispatcher::NOT_FOUND:
        http_response_code(404);
        break;
}

2. Type Shorthands

Pattern aliases expand during compilation into native PCRE2 patterns.

// Compiles to: /users/(?<id>\d+)
$compiler->get('/users/{id:int}', 'UserController@show');

// Compiles to RFC 4122 UUID pattern
$compiler->get('/files/{id:uuid}', 'FileController@show');

// Compiles to: /posts/(?<slug>[a-z0-9-]+)
$compiler->get('/posts/{slug:slug}', 'PostController@show');

Supported shorthands:

  • {var:int} $\rightarrow$ \d+
  • {var:uuid} $\rightarrow$ [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}
  • {var:slug} $\rightarrow$ [a-z0-9-]+
  • {var:alpha} $\rightarrow$ [a-zA-Z]+

3. Backed Enum Matching

String-backed PHP 8.1+ Enums translate into regex alternation groups (case1|case2) at compile time.

enum OrderStatus: string {
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
}

// Compiled regex constraint: (pending|paid|shipped)
$compiler->get('/orders/{status:' . OrderStatus::class . '}', 'OrderController@index');

4. Route Grouping

Prefix concatenation occurs strictly during compilation.

$compiler->addGroup('/api/v1', function (WajhaCompiler $api) {
    // Path: /api/v1/products
    $api->get('/products', 'ProductController@index');

    // Path: /api/v1/admin/stats
    $api->addGroup('/admin', function (WajhaCompiler $admin) {
        $admin->get('/stats', 'AdminController@stats');
    });
});

5. Optional Path Segments

Optional segments marked with square brackets ([/...]) expand recursively at compile time.

// Compiles into two discrete routes: /archive and /archive/{year:\d+}
$compiler->get('/archive[/{year:int}]', 'ArchiveController@index');

Benchmarks

Evaluated on PHP 8.5 over 100,000 request dispatch iterations against a 1,000 route dataset (70% valid routes, 15% method mismatches, 15% 404 targets):

Engine Throughput Avg Latency Speed Ratio
Safi/Wajha 447,171 req/s 2.236 µs Baseline (1.00x)
nikic/FastRoute 129,006 req/s 7.752 µs 3.47x slower
Phroute 120,615 req/s 8.291 µs 3.71x slower
Symfony Routing 107,682 req/s 9.287 µs 4.15x slower
AltoRouter 4,205 req/s 237.793 µs 106.33x slower

For detailed insights into the architecture, Zend VM micro-benchmarks, and real-world scenario trade-offs, see the write-up: Writing a PHP 8.5 Router Faster Than FastRoute

Running Benchmarks

composer require --dev nikic/fast-route symfony/routing phroute/phroute altorouter/altorouter
php tests/test.php
php tests/run_realworld_bench.php

License

MIT License. See LICENSE.md for details.