infocyph / webrick
A fast, modern PHP router with production-grade middleware, signed & temporary URLs, smart responses and first-class route caching.
Requires
- php: >=8.4
- infocyph/arraykit: ^4.6.1
- infocyph/intermix: ^7.3.2
- psr/cache: ^3.0
- psr/log: ^3.0.2
Requires (Dev)
- infocyph/cachelayer: ^2.0.1
- infocyph/phpforge: dev-main@dev
Suggests
- infocyph/cachelayer: Required by response-cache middleware and the default throttle cache backend.
- open-telemetry/exporter-otlp: Export traces to OpenTelemetry collectors (Jaeger, Zipkin, etc.)
- open-telemetry/sdk: Full OpenTelemetry tracing with automatic span export (auto-detected by TelemetryMiddleware)
README
A framework-neutral HTTP routing kernel for PHP with deploy-time route compilation, lazy middleware resolution, signed URLs, response helpers and emitters for traditional and persistent runtimes.
Highlights
- Fast routing: named routes, groups, domains, resources, attribute discovery
- Framework-neutral: run Webrick standalone or mount it behind another framework's request/response adapter
- Deploy-time compilation: sharded, fused and generated PHP route-cache artifacts
- Validated cache publication: exact format versions, staged PHP validation and atomic activation
- Lean cached dispatch: class handlers and string middleware stay scalar until the matched route is materialized
- Safe callable caching: public static first-class callables become scalar descriptors; stateful closures keep serializer semantics
- Lazy optional services: middleware alias families and URL generation are resolved only when used
- DI-aware dispatch: constructor and method injection through InterMix, with request scopes and service providers
- Signed URLs: permanent, TTL-based, or explicit-expiry links
- Rich signing controls: relative or absolute payloads, ignored query params, key rotation, custom signature params and algorithms
- Central error boundary: framework middleware throws typed HTTP exceptions and the kernel renders them just before emission
- User controllers and user middleware can still return
Responsedirectly; only framework-owned rejection paths are exception-driven - Response helpers: JSON, plaintext, redirects, streaming, ranged file/download responses, views
- Middleware pipeline: negotiation, compression, throttling, validators, telemetry, cookie encryption and more
- Runtime emitters: PHP-FPM, FrankenPHP, LiteSpeed, Nginx Unit, CLI, Swoole, RoadRunner and Workerman
Requirements
- PHP 8.4+
- Composer 2.x
Installation
composer require infocyph/webrick
Core routing does not install or initialize CacheLayer. Add CacheLayer 2 only when using
ResponseCacheMiddleware, the default throttle cache backend, or CacheLayer's
atomic counter backend:
composer require "infocyph/cachelayer:^2.0.1"
ThrottleMiddleware can also run without CacheLayer when the application
supplies its own PSR-6 cache pool.
Webrick is a library, not an application skeleton. Its directory layout, configuration source, container composition and deployment entry point remain under the host application's control.
Minimal Boot Example
<?php declare(strict_types=1); use Infocyph\Webrick\Request\Request; use Infocyph\Webrick\Response\Emitter\AutoEmitter; use Infocyph\Webrick\Response\Response; use Infocyph\Webrick\Router\Definition\Registrar; use Infocyph\Webrick\Router\Facade\Router as Route; use Infocyph\Webrick\Router\Kernel\RouterKernel; use Infocyph\Webrick\Router\Matching\ShardedMatcher; use Psr\Log\NullLogger; require __DIR__ . '/vendor/autoload.php'; $kernel = RouterKernel::bootWithRegistrar( log: new NullLogger(), matcher: ShardedMatcher::make(), register: static function (Registrar $registrar): void { unset($registrar); Route::get('/', static fn() => Response::plaintext('Hello Webrick', 200), 'home'); Route::get('/api/users/{id:int}', static fn(Request $request, string $id) => Response::json([ 'id' => (int) $id, 'method' => $request->getMethod(), ]), 'users.show'); }, routeCache: __DIR__ . '/.route-cache', ); (new AutoEmitter())->emit($kernel->handle(Request::fromGlobals()));
For deployable route caches, prefer a controller class-string or
[Controller::class, 'method'] handler. Static controller methods avoid a
controller allocation; non-static methods are constructed through InterMix.
Closures and object-backed handlers remain supported through the serializer
fallback, but they are not the cheapest cache representation.
Route-cache builds validate staged executable PHP before activation. Sharded
builds publish an immutable generation through a small manifest, so an
incomplete build cannot replace the generation used by live workers.
On systems that support symbolic links, a tiny atomic __current pointer keeps
generation selection out of PHP cache hydration; the manifest remains the
portable fallback.
Use Inside Another Framework
Keep one RouterKernel for the application or worker lifecycle and adapt only at
the HTTP boundary:
$webrickRequest = $requestAdapter->toWebrick($frameworkRequest); $webrickResponse = $kernel->handle($webrickRequest); return $responseAdapter->fromWebrick($webrickResponse);
Webrick's Request and Response expose familiar PSR-7-style message methods,
but they do not implement the PSR-7 interfaces. A host framework must therefore
provide the explicit adapters shown above. Do not emit the response from
Webrick when the host framework owns emission.
See documentation for container, middleware, request-scope and persistent-worker guidance.
Production-Oriented Boot Example
<?php declare(strict_types=1); use Infocyph\Webrick\Middleware\CompressionMiddleware; use Infocyph\Webrick\Middleware\GatewayHardeningMiddleware; use Infocyph\Webrick\Middleware\NegotiationMiddleware; use Infocyph\Webrick\Middleware\ThrottleMiddleware; use Infocyph\Webrick\Middleware\VerifySignedUrlMiddleware; use Infocyph\Webrick\Request\Request; use Infocyph\Webrick\Response\Emitter\AutoEmitter; use Infocyph\Webrick\Response\Response; use Infocyph\Webrick\Router\Definition\Registrar; use Infocyph\Webrick\Router\Dispatch\MiddlewareAliases; use Infocyph\Webrick\Router\Facade\Router as Route; use Infocyph\Webrick\Router\Kernel\RouterKernel; use Infocyph\Webrick\Router\Matching\ShardedMatcher; use Infocyph\Webrick\Router\Url\SignedUrlConfig; use Psr\Log\NullLogger; require __DIR__ . '/vendor/autoload.php'; $signKey = $_ENV['WEBRICK_SIGN_KEY'] ?? 'change-me'; $baseUri = $_ENV['WEBRICK_URL_BASE_URI'] ?? 'http://localhost'; $signedUrls = new SignedUrlConfig( generationKey: $signKey, verificationKeys: [$signKey], defaultTtl: 900, ); MiddlewareAliases::register( 'throttle', static fn(...$params) => new ThrottleMiddleware( max: (int) ($params[0] ?? 60), window: (int) ($params[1] ?? 60), ), ); MiddlewareAliases::register( 'verifySignedUrl', static fn() => new VerifySignedUrlMiddleware($signKey, 5), ); $kernel = RouterKernel::bootWithRegistrar( log: new NullLogger(), matcher: ShardedMatcher::make(), register: static function (Registrar $registrar): void { unset($registrar); Route::get('/users/{id:int}', fn(string $id) => Response::json(['id' => (int) $id]), 'users.show'); Route::get('/files/{file}', fn(string $file) => Response::attachment(__DIR__ . '/files/' . $file, $file), [ 'as' => 'files.show', 'middleware' => ['verifySignedUrl'], ]); }, routeCache: __DIR__ . '/.route-cache', registrarOptions: [ 'exposeUrlServices' => true, 'signKey' => $signKey, 'signedDefaultTtl' => 900, 'signedUrlConfig' => $signedUrls, 'urlBaseUri' => $baseUri, ], preGlobal: [ GatewayHardeningMiddleware::class, NegotiationMiddleware::class, ], postGlobal: [ CompressionMiddleware::class, ], fallbackAliasesFromRegistrar: true, ); (new AutoEmitter())->emit($kernel->handle(Request::fromGlobals()));
The signing configuration is preserved in registrarOptions. On cached boot,
the default URL binding defers alias loading and UrlGenerator construction
until the first URL helper call. Supply bindUrlServices only when an
integration needs to replace that default binding behavior.
URL Generation
use DateTimeImmutable; use Infocyph\Webrick\Router\Facade\Router as Route; use Infocyph\Webrick\Router\Url\SignedUrlConfig; $url = Route::urlFor('users.show', ['id' => 42]); $absolute = Route::urlFor('users.show', ['id' => 42], absolute: true); $signed = Route::signedUrlFor('files.show', ['file' => 'report.pdf']); $temp = Route::temporaryUrlFor('files.show', ['file' => 'report.pdf'], ttl: 900); $until = Route::temporaryUrlUntil('files.show', new DateTimeImmutable('+15 minutes'), ['file' => 'report.pdf']); $absolutePayload = Route::signedUrlFor( 'files.show', ['file' => 'report.pdf'], absolute: true, payloadMode: SignedUrlConfig::MODE_ABSOLUTE, );
Framework-owned failures such as invalid signed URLs, negotiation failures, throttling, request limits, bad Host headers and maintenance mode now throw typed HTTP exceptions internally. RouterKernel catches them at the top-level error boundary and renders the final HTTP response there. Your controllers and user middleware can still return Response objects with explicit status codes directly.
The demo app also includes /api/error-demo, which throws a framework HTTP exception and is rendered as JSON through a custom error boundary override.
You can also customize the final exception-to-response conversion by supplying your own ErrorHandler:
use Infocyph\Webrick\Request\Request; use Infocyph\Webrick\Response\Response; use Infocyph\Webrick\Router\Kernel\ErrorHandler; use Throwable; $errorHandler = new ErrorHandler( responseRenderer: static function (Request $request, Throwable $e, int $status, array $headers): ?Response { if (!str_starts_with($request->getUri()->getPath(), '/api/')) { return null; } return Response::json([ 'error' => $e->getMessage(), 'status' => $status, 'path' => $request->getUri()->getPath(), ], $status, $headers); }, );
Route Cache
Build cache artifacts during CI or deploy. Cache creation may spend more work so the request path does less:
php ./webrick route:cache --matcher=sharded --cache=.route-cache --routes=routes.php php ./webrick route:cache --matcher=fused --cache=.route-cache/fused.php --routes=routes.php php ./webrick route:cache --matcher=generated --cache=.route-cache/generated.php --routes=routes.php
Clear them when needed:
php ./webrick route:clear --matcher=sharded --cache=.route-cache
All matcher factories are zero-argument. Pass the cache directory or file only
as routeCache: when booting the kernel.
For the major release, rebuild every route-cache artifact after updating; cache formats are internal deployment artifacts and are not portable across major versions.
Security
Protected by PHPForge — an automated quality and security gate for PHP projects.
Made with ❤️ for the PHP communityMIT Licensed
Documentation • Security • Code of Conduct • Contributing • Report | Request | Suggest