infocyph/webrick

A fast, modern PHP router with production-grade middleware, signed & temporary URLs, smart responses and first-class route caching.

Maintainers

Package info

github.com/infocyph/Webrick

pkg:composer/infocyph/webrick

Transparency log

Statistics

Installs: 2 808

Dependents: 1

Suggesters: 0

Stars: 1

Open Issues: 0

3.2.2 2026-08-01 05:06 UTC

This package is auto-updated.

Last update: 2026-08-04 16:19:50 UTC


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.

Security & Standards Packagist Downloads License: MIT Packagist Version Packagist PHP Version GitHub Code Size Documentation

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 Response directly; 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 community
MIT Licensed
DocumentationSecurityCode of ConductContributingReport | Request | Suggest