PHPackages                             phpdot/routing - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. phpdot/routing

ActiveLibrary[HTTP &amp; Networking](/categories/http)

phpdot/routing
==============

High-performance segment-trie routing for PHP. PSR-7/15/17 compliant.

v1.3.1(2mo ago)0191MITPHPPHP &gt;=8.3

Since Apr 1Pushed 2mo agoCompare

[ Source](https://github.com/phpdot/routing)[ Packagist](https://packagist.org/packages/phpdot/routing)[ RSS](/packages/phpdot-routing/feed)WikiDiscussions main Synced 4w ago

READMEChangelogDependencies (21)Versions (6)Used By (1)

phpdot/routing
==============

[](#phpdotrouting)

High-performance segment-trie router for PHP. PSR-7/15/17 compliant.

Install
-------

[](#install)

```
composer require phpdot/routing
```

Quick Start
-----------

[](#quick-start)

```
use PHPdot\Routing\Router;

$router = new Router($container, $responseFactory);

$router->get('/users', [UserController::class, 'index']);
$router->get('/users/{id:int}', [UserController::class, 'show']);
$router->post('/users', [UserController::class, 'store']);

$response = $router->handle($request);
```

One class. Three lines of route registration. One line of dispatch.

### Inside the phpdot framework

[](#inside-the-phpdot-framework)

`Router` carries `#[Singleton]`, so when used with `phpdot/package` it's auto-wired by the container — no manual `new Router(...)` needed. Both constructor dependencies (`Psr\Container\ContainerInterface` and `Psr\Http\Message\ResponseFactoryInterface`) are resolved automatically (the latter via `phpdot/http`'s default binding).

```
$container = (new ContainerBuilder())
    ->addDefinitionsFromFile(vendor('phpdot/definitions.php'))
    ->build();

$router = $container->get(Router::class);
$router->get('/', [HomeController::class, 'index']);
$response = $router->handle($request);
```

The `#[Singleton]` attribute is inert at runtime — `phpdot/container` is a `require-dev` only. Standalone consumers (no DI framework) instantiate `Router` directly via the constructor as shown above.

---

Architecture
------------

[](#architecture)

### Request/Response Lifecycle

[](#requestresponse-lifecycle)

```
                          BOOT (once per worker)
 ┌─────────────────────────────────────────────────────────────┐
 │                                                             │
 │   Register routes (fluent API)                              │
 │      $router->get('/users/{id:int}', ...)                   │
 │      $router->group('/api', function () { ... })            │
 │                        │                                    │
 │                        ▼                                    │
 │              RouteCollection                                │
 │           (flat list of Route objects)                       │
 │                        │                                    │
 │                        ▼                                    │
 │             RouteCompiler::compile()                         │
 │                        │                                    │
 │                        ▼                                    │
 │                   TrieNode tree                              │
 │            (indexed by path segments)                        │
 │                        │                                    │
 │                        ▼                                    │
 │                   TrieMatcher                                │
 │              (ready to match requests)                       │
 │                                                             │
 └─────────────────────────────────────────────────────────────┘

                      REQUEST (per coroutine)
 ┌─────────────────────────────────────────────────────────────┐
 │                                                             │
 │   ServerRequestInterface                                    │
 │      GET /users/42                                          │
 │                        │                                    │
 │                        ▼                                    │
 │              Router::handle($request)                        │
 │                        │                                    │
 │                        ▼                                    │
 │              Path::segments('/users/42')                     │
 │              → ['users', '42']                               │
 │                        │                                    │
 │                        ▼                                    │
 │           TrieMatcher::match('GET', segments)                │
 │                        │                                    │
 │              ┌─────────┼──────────┐                         │
 │              ▼         ▼          ▼                         │
 │          RouteMatch  405       null                         │
 │              │     MethodNot   (not found)                   │
 │              │     Allowed        │                         │
 │              │         │          ▼                         │
 │              │         │     fallback()                      │
 │              │         │     or 404                          │
 │              │         ▼                                    │
 │              │    Response 405                               │
 │              │    + Allow header                             │
 │              ▼                                              │
 │     Middleware Pipeline                                      │
 │              │                                              │
 │     ┌────────┼────────┐                                     │
 │     ▼        ▼        ▼                                     │
 │   MW 1 → MW 2 → ... → Route Handler                        │
 │     │        │        │       │                             │
 │     │        │        │       ▼                             │
 │     │        │        │   Controller::show($request, 42)    │
 │     │        │        │       │                             │
 │     ◄────────◄────────◄───────┘                             │
 │              │                                              │
 │              ▼                                              │
 │       ResponseInterface                                     │
 │                                                             │
 └─────────────────────────────────────────────────────────────┘

```

### Trie Matching

[](#trie-matching)

Routes are compiled into a segment trie at boot. Matching walks one node per URL segment — O(depth), not O(routes).

```
Routes:
  GET  /users
  GET  /users/{id:int}
  GET  /users/{id:int}/posts
  POST /users/{id:int}/posts
  GET  /api/v1/health
  GET  /docs/{path:*}

Trie:
  root
  ├── "users" ────────────────── [GET → Route#1]
  │   └── {id:int} ──────────── [GET → Route#2]
  │       └── "posts" ────────── [GET → Route#3, POST → Route#4]
  │
  ├── "api"
  │   └── "v1"
  │       └── "health" ────────── [GET → Route#5]
  │
  └── "docs"
      └── {path:*} ────────────── [GET → Route#6]

```

Matching `GET /users/42/posts`:

```
Step 1: root → "users"   (hash lookup)
Step 2: "users" → "42"   (regex: [0-9]+ matches)
Step 3: {id} → "posts"   (hash lookup)
Step 4: leaf has GET?     → Route#3, params: {id: 42}

```

3 lookups. Same speed whether you have 10 routes or 1,000.

### Middleware Pipeline (PSR-15)

[](#middleware-pipeline-psr-15)

Middleware wraps the handler inside-out. Each middleware can modify the request before and the response after.

```
Request
  │
  ▼
┌──────────────────────────────────────┐
│ Middleware 1                         │
│   before: log request                │
│   ┌──────────────────────────────┐   │
│   │ Middleware 2                 │   │
│   │   before: check auth         │   │
│   │   ┌──────────────────────┐   │   │
│   │   │ Route Handler        │   │   │
│   │   │   return Response    │   │   │
│   │   └──────────────────────┘   │   │
│   │   after: add CORS headers    │   │
│   └──────────────────────────────┘   │
│   after: log response                │
└──────────────────────────────────────┘
  │
  ▼
Response

```

Middleware can short-circuit by returning a response without calling `$handler->handle()`.

---

Package Structure
-----------------

[](#package-structure)

```
src/
├── Router.php                      Main entry point
│
├── Route/
│   ├── Route.php                   Immutable route definition
│   ├── RouteCollection.php         Flat list of all routes
│   ├── RouteGroup.php              Fluent group builder
│   └── RouteScope.php              Reusable preset bundle
│
├── Compiler/
│   ├── RouteCompiler.php           RouteCollection → TrieNode tree
│   └── PatternRegistry.php         Named regex patterns
│
├── Matcher/
│   ├── MatcherInterface.php        Contract for matchers
│   ├── TrieMatcher.php             Walks compiled trie
│   ├── TrieNode.php                Trie node structure
│   ├── RouteMatch.php              Successful match result
│   └── MethodNotAllowed.php        405 result with allowed methods
│
├── Generator/
│   └── UrlGenerator.php            Named route → URL reversal
│
├── Contracts/
│   ├── ControllerInterface.php     Marker for controller classes
│   └── RouteRegistrarInterface.php Contract for route registration
│
├── Traits/
│   └── HttpMethodsTrait.php        get(), post(), put(), etc.
│
└── Utils/
    └── Path.php                    URL path utilities

```

---

Route Registration
------------------

[](#route-registration)

### Basic Routes

[](#basic-routes)

```
$router->get('/users', [UserController::class, 'index']);
$router->post('/users', [UserController::class, 'store']);
$router->put('/users/{id:int}', [UserController::class, 'update']);
$router->patch('/users/{id:int}', [UserController::class, 'update']);
$router->delete('/users/{id:int}', [UserController::class, 'destroy']);
```

### Closure Handlers

[](#closure-handlers)

```
$router->get('/health', function (ServerRequestInterface $request): ResponseInterface {
    return new Response(200, [], json_encode(['status' => 'ok']));
});
```

### String Handlers

[](#string-handlers)

```
$router->get('/users', 'App\Controllers\UserController@index');
```

### Route Parameters

[](#route-parameters)

```
$router->get('/users/{id:int}', ...);           // integer
$router->get('/posts/{slug:slug}', ...);         // slug (a-z, 0-9, hyphens)
$router->get('/files/{name}', ...);              // any (no constraint)
$router->get('/items/{uuid:uuid4}', ...);        // UUID v4
$router->get('/docs/{id:mongo_id}', ...);        // MongoDB ObjectId
```

### Optional Parameters

[](#optional-parameters)

```
$router->get('/posts/{page:int?}', function (ServerRequestInterface $req, int $page = 1): ResponseInterface {
    // GET /posts     → page = 1
    // GET /posts/3   → page = 3
});
```

Optional works at any position — beginning, middle, or end:

```
$router->get('/{lang:locale?}/users', ...);
// GET /users      → lang not set
// GET /en/users   → lang = "en"
// GET /ar/users   → lang = "ar"
```

### Wildcard (Catch-All)

[](#wildcard-catch-all)

```
$router->get('/docs/{path:*}', function (ServerRequestInterface $req, string $path): ResponseInterface {
    // GET /docs/guide/install → path = "guide/install"
});
```

### Route Naming

[](#route-naming)

```
$router->get('/users/{id:int}', [UserController::class, 'show'])->name('users.show');

// Generate URL
$router->url('users.show', ['id' => 42]);                        // /users/42
$router->url('users.show', ['id' => 42], ['tab' => 'posts']);    // /users/42?tab=posts
```

### Where Constraints

[](#where-constraints)

```
$router->get('/items/{code}', [ItemController::class, 'show'])
    ->where('code', 'slug');
```

### Custom Patterns

[](#custom-patterns)

```
$router->addPattern('short_id', '[a-zA-Z0-9]{8}');
$router->get('/links/{code:short_id}', [LinkController::class, 'redirect']);
```

---

Groups
------

[](#groups)

```
$router->group('/api/v1', function (RouteGroup $group): void {
    $group->get('/users', [UserController::class, 'index']);
    $group->get('/users/{id:int}', [UserController::class, 'show']);
    $group->post('/users', [UserController::class, 'store']);

    $group->group('/admin', function (RouteGroup $admin): void {
        $admin->get('/stats', [StatsController::class, 'index']);
    });
})->middleware(AuthMiddleware::class);
```

Groups accumulate prefixes and middleware. Nested groups inherit from their parent.

---

Middleware
----------

[](#middleware)

Middleware implements PSR-15 `MiddlewareInterface`:

```
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class AuthMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $token = $request->getHeaderLine('Authorization');

        if ($token === '') {
            return new Response(401, [], 'Unauthorized');
        }

        return $handler->handle($request);
    }
}
```

### Global Middleware

[](#global-middleware)

```
$router->middleware(CorsMiddleware::class);
$router->middleware(AuthMiddleware::class);
```

Runs in registration order for every matched route.

### Route Middleware

[](#route-middleware)

```
$router->get('/admin/stats', [StatsController::class, 'index'])
    ->middleware(AdminMiddleware::class);
```

### Closure Middleware

[](#closure-middleware)

```
$router->middleware(function (ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
    $response = $handler->handle($request);
    return $response->withHeader('X-Request-Id', uniqid());
});
```

---

Scopes
------

[](#scopes)

Reusable preset bundles of middleware and hosts:

```
$scope = new RouteScope('api');
$scope->middleware(AuthMiddleware::class);
$scope->middleware(RateLimitMiddleware::class);
$scope->host('api.example.com');

$router->addScope($scope);

$router->get('/data', [DataController::class, 'index'])
    ->scope($router->getScope('api'));
```

---

Host Routing
------------

[](#host-routing)

```
$router->get('/dashboard', [DashboardController::class, 'index'])
    ->host('admin.example.com');
```

---

Base Path (subfolder deployments)
---------------------------------

[](#base-path-subfolder-deployments)

Deploy at `http://example.com/site/admin/` or `http://example.com/api/v1/`? Set the base path once and routes stay deployment-agnostic:

```
$router->setBasePath('/site/admin');

$router->get('/', [DashboardController::class, 'index']);
$router->get('/users/{id:int}', [UserController::class, 'show']);
```

Routes match against the path **after** the base is stripped, so `/site/admin/` resolves to `/`, `/site/admin/users/42` resolves to `/users/42`. Multi-segment base paths work the same way.

The base path is stripped from incoming request URIs before route matching. Routes never reference the deployment URL — only your `setBasePath()` call does.

```
$router->setBasePath((string) env('APP_BASE_PATH', ''));
```

**Why explicit, not auto-detected:** auto-detection via `$_SERVER['SCRIPT_NAME']` is FPM-specific and breaks under Swoole (where `$_SERVER` is populated once at worker boot, not per-request). `setBasePath()` is runtime-agnostic — works under FPM, Swoole, RoadRunner, FrankenPHP, etc.

`setBasePath()` normalises slashes (`'site/admin'`, `'/site/admin'`, `'/site/admin/'` all become `/site/admin`); the empty string disables stripping. Requests outside the configured base path return 404 (strict — `/site/admin` does not match `/site/administrators/foo`).

---

Fallback Handler
----------------

[](#fallback-handler)

```
$router->fallback(function (ServerRequestInterface $request): ResponseInterface {
    return new Response(404, [], json_encode(['error' => 'Not Found']));
});
```

---

Controllers
-----------

[](#controllers)

Controllers must implement `ControllerInterface`:

```
use PHPdot\Routing\Contracts\ControllerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class UserController implements ControllerInterface
{
    public function show(ServerRequestInterface $request, int $id): ResponseInterface
    {
        return new Response(200, [], json_encode(['id' => $id]));
    }
}
```

Route parameters are passed as method arguments. The request also carries them as attributes:

```
$request->getAttribute('id');           // 42
$request->getAttribute('_route');       // Route object
$request->getAttribute('_route_params'); // ['id' => 42]
```

---

Path Utilities
--------------

[](#path-utilities)

```
use PHPdot\Routing\Utils\Path;

Path::segments('/users/42/posts');  // ['users', '42', 'posts']
Path::build(['users', '42']);       // /users/42
Path::first('/en/users');           // 'en'
Path::shift('/en/users/42');        // /users/42
```

---

PSR Standards
-------------

[](#psr-standards)

PSRInterfaceUsagePSR-7`ServerRequestInterface`Request input for matching and dispatchPSR-7`ResponseInterface`Handler and middleware return typePSR-11`ContainerInterface`Resolves controllers and middlewarePSR-15`RequestHandlerInterface`Router implements this — `$router->handle($request)`PSR-15`MiddlewareInterface`Standard middleware contractPSR-17`ResponseFactoryInterface`Creates 404/405 responses---

Performance
-----------

[](#performance)

Compilation happens once at boot. Matching is constant-time regardless of route count.

ScenarioLatencyThroughputStatic routes~0.6 µs1.6M matches/secDynamic routes~0.85 µs1.2M matches/secWorst case (1000 routes)~0.78 µs1.3M matches/sec404 not found~0.34 µs2.9M matches/secCompilation (1000 routes)~1.3 msonce at boot---

Development
-----------

[](#development)

```
composer test        # Run tests
composer analyse     # PHPStan level 10
composer cs-fix      # Fix code style
composer cs-check    # Check code style (dry run)
composer check       # Run all three
```

License
-------

[](#license)

MIT

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance87

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~7 days

Total

5

Last Release

63d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/62e82421bda4b5d6ba9a47ba6d88caca060dcd0d1a2862f351f3a97657385db0?d=identicon)[phpdot](/maintainers/phpdot)

---

Top Contributors

[![phpdot](https://avatars.githubusercontent.com/u/252500?v=4)](https://github.com/phpdot "phpdot (5 commits)")

---

Tags

httppsr-7middlewarerouterroutingpsr-15trie

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/phpdot-routing/health.svg)

```
[![Health](https://phpackages.com/badges/phpdot-routing/health.svg)](https://phpackages.com/packages/phpdot-routing)
```

###  Alternatives

[cakephp/cakephp

The CakePHP framework

8.9k19.5M1.8k](/packages/cakephp-cakephp)[guzzlehttp/psr7

PSR-7 message implementation that also provides common utility methods

8.0k1.1B3.9k](/packages/guzzlehttp-psr7)[mezzio/mezzio

PSR-15 Middleware Microframework

3923.8M121](/packages/mezzio-mezzio)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k13](/packages/tempest-framework)[typo3/cms-core

TYPO3 CMS Core

3713.2M5.0k](/packages/typo3-cms-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
