Architecture Notes
Design Principles
Request Flow
HTTP Request
-> JsonRpcServer (HTTP facade)
-> RequestReader (body extraction, gzip decoding, size limit)
-> JsonRpcEngine (core, transport-agnostic)
-> HookManager (fire BEFORE_REQUEST)
-> JSON Decode (with depth limit)
-> BatchProcessor (detect single vs batch, validate structure, enforce batch.max_items)
-> AuthManager + RequestAuthenticator (extract credentials, authenticate, build UserContext)
-> RateLimitManager (check rate limits)
-> For each request:
-> HookManager (fire BEFORE_HANDLER)
-> SchemaValidator (optional, if handler provides schemas)
-> MiddlewarePipeline (execute middleware stack around handler execution)
-> MethodResolver (map method name to handler class+method)
-> HandlerDispatcher + HandlerFactory (instantiate handler, invoke method)
-> ParameterBinder (bind params to method arguments)
-> HookManager (fire AFTER_HANDLER) -- fires inside handler execution
-> Response building (success or error)
-> HookManager (fire ON_RESPONSE)
-> HttpResponse (JSON encode, optional ETag, optional gzip)
Lifecycle Guarantees
The execution order is formalized and tested. The following guarantees hold for every request:
Before middleware runs:
BEFORE_HANDLER has already fired for the requestRequestContext is available with correlation ID, headers, client IP, and auth stateDuring middleware execution:
Request objectRequestContext contains all authentication informationResponse without calling $next()BEFORE_HANDLER has already fired before the first middleware runsAFTER_HANDLER hook fires after handler execution, before the middleware's post-processingAfter middleware completes:
Response object or null (for notifications)Hook execution order (per request):
BEFORE_REQUEST -> [auth hooks] -> [rate limit] -> BEFORE_HANDLER -> [schema validation] -> [middleware wraps: handler] -> AFTER_HANDLER -> ON_RESPONSE -> AFTER_REQUEST
What is NOT guaranteed before middleware:
Component Responsibilities
Core (src/Core/)
JsonRpcEngine: Transport-agnostic JSON-RPC processing engine (decode, dispatch, auth, rate limit, middleware, hooks)EngineResult: Value object for engine output (JSON string or null, status code, headers)Config (src/Config/)
Config: Dot-notation access to nested configurationDefaults: Complete default configuration valuesProtocol (src/Protocol/)
Request: Immutable value object for JSON-RPC requestsResponse: Immutable value object for JSON-RPC responsesError: Immutable value object for JSON-RPC errorsRequestValidator: Validates request structure against specBatchProcessor: Handles single vs batch detection and processingBatchResult: Result of batch processing (requests + validation errors)Http (src/Http/)
HttpRequest: Wraps PHP globals into a testable request objectHttpResponse: Represents an HTTP response with body, status, headersRequestReader: Reads raw body, handles compression and size limitsDispatcher (src/Dispatcher/)
MethodResolver: Maps handler.method to class file + method name safelyHandlerDispatcher: Creates handler instances and invokes methodsHandlerFactoryInterface: Contract for custom handler instantiation (DI)DefaultHandlerFactory: Default factory (RequestContext injection, optional params)ParameterBinder: Binds positional or named params to method argumentsHandlerRegistry: Discovers all available handlers and methods from top-level handler files in configured pathsProcedureDescriptor: Value object for explicit procedure registration with metadataMethodResolution: Value object representing a resolved handler class + methodAuth (src/Auth/)
AuthManager: Orchestrates authentication (enabled/disabled, token extraction)AuthenticatorInterface: Low-level token validator contractJwtAuthenticator: Decodes and validates JWT tokens (HMAC)RequestAuthenticatorInterface: Driver-level auth from HTTP headersJwtRequestAuthenticator: JWT driver (extracts Bearer token from header)ApiKeyAuthenticator: API key driver (reads key from configurable header)BasicAuthenticator: HTTP Basic driver (username/password from Authorization header)UserContext: Immutable authenticated user context with rolesMiddleware (src/Middleware/)
MiddlewareInterface: Contract for request middlewareMiddlewarePipeline: Ordered middleware execution with short-circuit supportValidation (src/Validation/)
RpcSchemaProviderInterface: Contract for handlers declaring parameter schemasSchemaValidator: Lightweight JSON Schema subset validatorRateLimit (src/RateLimit/)
RateLimitManager: Orchestrates rate limiting by strategyRateLimiterInterface: Pluggable contract for rate limit backendsFileRateLimiter: File-based rate limit storage with atomic weighted consumption and configurable fail-open/fail-closed behaviorInMemoryRateLimiter: In-memory rate limiter for testing or single-process useLog (src/Log/)
Logger: Writes structured logs with level filteringLogRotator: Rotates, compresses, and prunes log filesLogFormatter: Formats log lines with optional secret sanitizationCache (src/Cache/)
ResponseFingerprinter: Generates content hashes for ETag supportDoc (src/Doc/)
DocGenerator: Extracts documentation from handlers via reflection + PHPDoc. Uses already-discovered handlers from HandlerRegistry — does not mutate registry state.MarkdownGenerator: Renders docs as MarkdownHtmlGenerator: Renders docs as styled HTMLJsonDocGenerator: Renders docs as JSONOpenRpcGenerator: Renders docs as OpenRPC 1.3.2 specificationSupport (src/Support/)
RequestContext: Immutable request context (IP, headers, auth info, rawBody = transport-level body, requestBody = decoded body)HookManager: Registers and fires lifecycle hooksHookPoint: Enum of available hook pointsCompressor: Gzip encode/decode utilitiesCorrelationId: Generates unique request IDsError Handling Strategy
All errors are converted to proper JSON-RPC error responses:
| Scenario | Error Code | Message |
|---|---|---|
| Invalid JSON | -32700 | Parse error |
| Invalid request structure | -32600 | Invalid Request |
| Empty POST body | -32600 | Invalid Request |
Empty batch [] | -32600 | Invalid Request |
| Batch exceeds limit | -32600 | Invalid Request |
| Unknown method | -32601 | Method not found |
| Bad parameters | -32602 | Invalid params |
| Unknown/surplus params | -32602 | Invalid params |
| Server/application error | -32603 | Internal error |
| JSON serialization failed | -32603 | Internal error |
| Rate limit exceeded | -32000 | Rate limit exceeded |
| Authentication required | -32001 | Authentication required |
| Custom server errors | -32000 to -32099 | Implementation-defined |
In production mode (debug: false), stack traces and internal details are stripped from error responses. In debug mode, additional diagnostic data is included.
JSON serialization failures in response encoding are handled per-response: each response is encoded independently using JSON_THROW_ON_ERROR so one unserializable result does not affect other responses in a batch. A single failed response becomes a -32603 error with a diagnostic message, while other batch responses remain intact.
Documentation generators (OpenRpcGenerator, JsonDocGenerator) use JSON_THROW_ON_ERROR — encoding failures are surfaced as exceptions rather than silently replaced with empty JSON objects.
The FileRateLimiter uses JSON_THROW_ON_ERROR for persisting rate limit state — encoding failures surface as exceptions rather than silently corrupting state with empty JSON.
Extension Points
Custom Handler Factory (DI)
$server->setHandlerFactory(new class implements \Lumen\JsonRpc\Dispatcher\HandlerFactoryInterface {
public function create(string $className, \Lumen\JsonRpc\Support\RequestContext $context): object {
// inject dependencies before returning the handler instance
}
});
Middleware
$server->addMiddleware(new class implements \Lumen\JsonRpc\Middleware\MiddlewareInterface {
public function process(
\Lumen\JsonRpc\Protocol\Request $request,
\Lumen\JsonRpc\Support\RequestContext $context,
callable $next
): ?\Lumen\JsonRpc\Protocol\Response {
// pre-processing
$response = $next($request, $context);
// post-processing
return $response;
}
});
Custom Authenticator
Implement AuthenticatorInterface:
class MyAuth implements \Lumen\JsonRpc\Auth\AuthenticatorInterface {
public function authenticate(string $token): ?\Lumen\JsonRpc\Auth\UserContext {
// Custom auth logic
}
}
For custom header parsing or non-standard credential extraction, implement RequestAuthenticatorInterface and register it through JsonRpcServer::setRequestAuthenticator().
Custom Rate Limiter
Implement RateLimiterInterface:
class RedisRateLimiter implements \Lumen\JsonRpc\RateLimit\RateLimiterInterface {
public function check(string $key): \Lumen\JsonRpc\RateLimit\RateLimitResult {
// Redis-backed single check
}
public function checkAndConsume(string $key, int $weight): \Lumen\JsonRpc\RateLimit\RateLimitResult {
// Redis-backed atomic weighted check+consume
}
}
An InMemoryRateLimiter is also available for testing or single-process use cases.
Explicit Procedure Descriptors
For advanced use cases where you want an explicit API contract instead of (or in addition to) handler auto-discovery:
use Lumen\JsonRpc\Dispatcher\ProcedureDescriptor;
$registry = $server->getRegistry();
$registry->register('math.add', MathHandler::class, 'add', [
'description' => 'Add two numbers',
'requiresAuth' => false,
]);
// Or using descriptor objects:
$registry->registerDescriptor(new ProcedureDescriptor(
method: 'math.multiply',
handlerClass: MathHandler::class,
handlerMethod: 'multiply',
metadata: ['description' => 'Multiply two numbers'],
));
Descriptor metadata is used by the documentation generators (including OpenRPC). Runtime request schemas from RpcSchemaProviderInterface are also reused by the JSON and OpenRPC generators when available.
For richer response contracts, explicit descriptor metadata may include resultSchema, and auto-discovered handlers may provide @result-schema {...} in method docblocks; both flow through to generated JSON/OpenRPC result definitions.
OpenRPC Export
Generate machine-readable OpenRPC specification:
php bin/generate-docs.php --config=./config.php --format=openrpc --output=docs/openrpc.json
Hooks
$server->getHooks()->register(
\Lumen\JsonRpc\Support\HookPoint::BEFORE_HANDLER,
function (array $context) {
// Pre-processing logic
return ['extra_data' => 'value'];
}
);
Hooks run inline with request execution. By default, hook exceptions are isolated, logged, and skipped so later hooks and request handling can continue. Set hooks.isolate_exceptions to false if you need hook failures to abort the request.