|
| 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