Skip to content

Commit 0847823

Browse files
committed
docs: promote 7.x to current stable and cut 7.0.0 changelog
Copy the unstable docs snapshot to docs/7.x and rewrite its internal links, add 7.x as the current default release in releases.yml while demoting 6.x to old (still supported until 2026-12), and promote the CHANGELOG [Unreleased] block to [7.0.0]. The unstable docs line is left in place to continue tracking dev work.
1 parent 5302c8f commit 0847823

14 files changed

Lines changed: 2292 additions & 3 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
66

77
## [Unreleased]
88

9+
## [7.0.0] 2026-07-14
10+
911
### Added
1012
- `RouterInterface` extending PSR-15 `RequestHandlerInterface` for Router/Cache\Router substitutability.
1113
- `MatchResult` value object and `MatchStatus` enum for matching routes without dispatching (#328, #352).

docs/7.x/cached-router.md

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
---
2+
layout: post
3+
title: Cached Router
4+
sections:
5+
Introduction: introduction
6+
How It Works: how-it-works
7+
Usage: usage
8+
Cache Stores: cache-stores
9+
Cache Invalidation: cache-invalidation
10+
Using RouterInterface: using-routerinterface
11+
---
12+
## Introduction
13+
14+
Route provides a cached router implementation that significantly improves performance on larger applications by caching compiled FastRoute data. Unlike earlier versions, the cached router is now production-ready and no longer in BETA.
15+
16+
The `League\Route\Cache\Router` class works by caching the expensive FastRoute route compilation step, whilst still running your builder function on every request to ensure routes and handlers remain fresh. This means you get near-zero startup overhead on subsequent requests without sacrificing dynamic route registration or live handler resolution.
17+
18+
## How It Works
19+
20+
The cached router uses a builder pattern where you provide a callable that configures your router:
21+
22+
1. The builder function registers all your routes by calling `$router->map()`, `$router->group()`, etc.
23+
2. Routes are compiled using FastRoute, which is expensive and CPU-intensive.
24+
3. The compiled route data is hashed and cached in the storage backend.
25+
4. On subsequent requests, the compiled data is retrieved from cache, skipping the expensive compilation step.
26+
5. The builder runs again to ensure Route objects are fresh and your handlers are resolved at request-time.
27+
6. If the signature hash changes (routes were modified), the cache is automatically invalidated and rebuilt.
28+
7. If a cache file becomes corrupt, the router detects this and rebuilds the cache automatically.
29+
30+
This design ensures:
31+
32+
- Fast startup times after the first request
33+
- Reliable cache invalidation when routes change
34+
- Only plain PHP arrays (the compiled FastRoute route data) are serialised, not the entire Router object or any controller instances
35+
- Support for closures and container-resolved handlers, since closure-based controllers are never serialised
36+
- Automatic recovery from corruption
37+
38+
## Usage
39+
40+
Using the cached router is very similar to the standard router, but you pass a builder callable instead of configuring it directly:
41+
42+
~~~php
43+
<?php declare(strict_types=1);
44+
45+
use Psr\Http\Message\ResponseInterface;
46+
use Psr\Http\Message\ServerRequestInterface;
47+
48+
$cacheStore = new League\Route\Cache\FileCache('/path/to/cache/file.cache', 86400);
49+
50+
$cachedRouter = new League\Route\Cache\Router(
51+
function (League\Route\Router $router): League\Route\Router {
52+
$router->map('GET', '/', function (ServerRequestInterface $request): ResponseInterface {
53+
$response = new Laminas\Diactoros\Response;
54+
$response->getBody()->write('<h1>Hello, World!</h1>');
55+
return $response;
56+
});
57+
58+
$router->map('GET', '/users/{id}', 'UserController::show');
59+
60+
return $router;
61+
},
62+
$cacheStore
63+
);
64+
65+
$request = Laminas\Diactoros\ServerRequestFactory::fromGlobals(
66+
$_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
67+
);
68+
69+
$response = $cachedRouter->dispatch($request);
70+
71+
(new Laminas\HttpHandlerRunner\Emitter\SapiEmitter)->emit($response);
72+
~~~
73+
74+
On the first request, the builder will be invoked, routes compiled, and the result cached. On subsequent requests, the cached compiled data is used, and your builder is still called to instantiate fresh Route objects.
75+
76+
You can also pass optional parameters to customise caching behaviour:
77+
78+
~~~php
79+
<?php declare(strict_types=1);
80+
81+
$cachedRouter = new League\Route\Cache\Router(
82+
$builder,
83+
$cacheStore,
84+
cacheEnabled: true,
85+
cacheKey: 'my-custom-key'
86+
);
87+
~~~
88+
89+
- `cacheEnabled: bool` (default: true) - Set to false to disable caching temporarily
90+
- `cacheKey: string` (default: 'league/route/cache') - Custom cache key for multiple router instances
91+
92+
## Cache Stores
93+
94+
The cached router can use any [PSR-16](https://www.php-fig.org/psr/psr-16/) simple cache implementation. Route provides a file-based PSR-16 implementation, but you can use any PSR-16 compatible store.
95+
96+
### FileCache
97+
98+
Route includes a `League\Route\Cache\FileCache` implementation that stores the compiled route data in a file:
99+
100+
~~~php
101+
<?php declare(strict_types=1);
102+
103+
$cache = new League\Route\Cache\FileCache(
104+
'/path/to/cache/file.cache',
105+
86400
106+
);
107+
108+
$cachedRouter = new League\Route\Cache\Router($builder, $cache);
109+
~~~
110+
111+
The FileCache requires a writable directory and will automatically create the cache file. The second argument is the TTL in seconds.
112+
113+
### PSR-16 Compatible Stores
114+
115+
Any PSR-16 compatible cache implementation will work. Browse available implementations at [Packagist](https://packagist.org/providers/psr/simple-cache-implementation).
116+
117+
## Cache Invalidation
118+
119+
The cached router uses an intelligent signature hash to detect when routes have changed:
120+
121+
1. A hash is generated from the registered route methods and paths.
122+
2. This hash is stored with the cached data.
123+
3. When the router is instantiated, a new hash is generated from the current routes.
124+
4. If the hashes differ, the cache is invalidated and rebuilt.
125+
126+
This means you don't need to manually clear the cache when routes change. It happens automatically.
127+
128+
### Manual Cache Clearing
129+
130+
If you need to manually clear the cache (for example, during deployment or testing), you can use your cache store's `delete()` or `clear()` method:
131+
132+
~~~php
133+
<?php declare(strict_types=1);
134+
135+
$cache->clear();
136+
~~~
137+
138+
If you are using a PSR-16 store with named keys, use `delete()` with the matching key:
139+
140+
~~~php
141+
<?php declare(strict_types=1);
142+
143+
$cache->delete('league/route/cache');
144+
$cache->delete('my-custom-key');
145+
~~~
146+
147+
### Handling Corruption
148+
149+
If a cache file becomes corrupt or unreadable, the cached router automatically detects this and rebuilds the cache. This happens transparently without any configuration or manual intervention.
150+
151+
## Using RouterInterface
152+
153+
Both the standard `Router` and the `Cache\Router` implement the new `RouterInterface`, allowing you to easily swap between them without changing your application code.
154+
155+
Type-hint against `RouterInterface` in your dependency injection container:
156+
157+
~~~php
158+
<?php declare(strict_types=1);
159+
160+
use League\Route\RouterInterface;
161+
162+
$container = new League\Container\Container;
163+
164+
$container->add(
165+
RouterInterface::class,
166+
League\Route\Router::class
167+
);
168+
169+
$container->add(
170+
RouterInterface::class,
171+
function (): RouterInterface {
172+
return new League\Route\Cache\Router(
173+
function (League\Route\Router $router): League\Route\Router {
174+
return $router;
175+
},
176+
new League\Route\Cache\FileCache('/tmp/route.cache', 86400)
177+
);
178+
}
179+
);
180+
~~~
181+
182+
This enables you to switch between routers based on environment or configuration:
183+
184+
~~~php
185+
<?php declare(strict_types=1);
186+
187+
$cacheEnabled = $_ENV['ROUTE_CACHE'] ?? true;
188+
189+
if ($cacheEnabled) {
190+
$container->add(RouterInterface::class, function (): RouterInterface {
191+
return new League\Route\Cache\Router($builder, $cache);
192+
});
193+
} else {
194+
$container->add(RouterInterface::class, function (): RouterInterface {
195+
return $builder(new League\Route\Router);
196+
});
197+
}
198+
~~~
199+
200+
Now any service that type-hints against `RouterInterface` will work with either implementation.

0 commit comments

Comments
 (0)