Architecture Notes

Design Principles

  • Protocol compliance first - Core JSON-RPC 2.0 behavior is implemented with documented strict defaults and HTTP transport deviations (see Transport Behavior in README)
  • Clean separation - Each class has a single, focused responsibility
  • No framework lock-in - Pure PHP 8.x with minimal dependencies
  • Safe defaults - Security-conscious defaults (debug off, secrets sanitized, safe method resolution)
  • Explicit over magic - No dynamic method execution without validation
  • 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:

  • JSON has been decoded successfully (parse errors short-circuit before middleware)
  • Request structure has been validated (invalid requests short-circuit before middleware)
  • Batch detection has completed, batch limit has been enforced
  • Authentication has been resolved (if enabled and headers are present)
  • Rate limit has been checked (if enabled)
  • BEFORE_HANDLER has already fired for the request
  • Schema validation has already completed when a schema provider is present
  • RequestContext is available with correlation ID, headers, client IP, and auth state
  • During middleware execution:

  • Middleware receives a fully parsed and validated Request object
  • RequestContext contains all authentication information
  • Middleware executes in registration order (outer first, inner last)
  • Each middleware can short-circuit by returning a Response without calling $next()
  • BEFORE_HANDLER has already fired before the first middleware runs
  • AFTER_HANDLER hook fires after handler execution, before the middleware's post-processing
  • After middleware completes:

  • Handler has been invoked (unless middleware short-circuited)
  • Response is a valid JSON-RPC 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:

  • Method resolution has NOT occurred yet (method may still be not found)
  • Parameter binding has NOT occurred yet (params may be invalid)
  • 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 configuration
  • Defaults: Complete default configuration values
  • Protocol (src/Protocol/)

  • Request: Immutable value object for JSON-RPC requests
  • Response: Immutable value object for JSON-RPC responses
  • Error: Immutable value object for JSON-RPC errors
  • RequestValidator: Validates request structure against spec
  • BatchProcessor: Handles single vs batch detection and processing
  • BatchResult: Result of batch processing (requests + validation errors)
  • Http (src/Http/)

  • HttpRequest: Wraps PHP globals into a testable request object
  • HttpResponse: Represents an HTTP response with body, status, headers
  • RequestReader: Reads raw body, handles compression and size limits
  • Dispatcher (src/Dispatcher/)

  • MethodResolver: Maps handler.method to class file + method name safely
  • HandlerDispatcher: Creates handler instances and invokes methods
  • HandlerFactoryInterface: Contract for custom handler instantiation (DI)
  • DefaultHandlerFactory: Default factory (RequestContext injection, optional params)
  • ParameterBinder: Binds positional or named params to method arguments
  • HandlerRegistry: Discovers all available handlers and methods from top-level handler files in configured paths
  • ProcedureDescriptor: Value object for explicit procedure registration with metadata
  • MethodResolution: Value object representing a resolved handler class + method
  • Auth (src/Auth/)

  • AuthManager: Orchestrates authentication (enabled/disabled, token extraction)
  • AuthenticatorInterface: Low-level token validator contract
  • JwtAuthenticator: Decodes and validates JWT tokens (HMAC)
  • RequestAuthenticatorInterface: Driver-level auth from HTTP headers
  • JwtRequestAuthenticator: 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 roles
  • Middleware (src/Middleware/)

  • MiddlewareInterface: Contract for request middleware
  • MiddlewarePipeline: Ordered middleware execution with short-circuit support
  • Validation (src/Validation/)

  • RpcSchemaProviderInterface: Contract for handlers declaring parameter schemas
  • SchemaValidator: Lightweight JSON Schema subset validator
  • RateLimit (src/RateLimit/)

  • RateLimitManager: Orchestrates rate limiting by strategy
  • RateLimiterInterface: Pluggable contract for rate limit backends
  • FileRateLimiter: File-based rate limit storage with atomic weighted consumption and configurable fail-open/fail-closed behavior
  • InMemoryRateLimiter: In-memory rate limiter for testing or single-process use
  • Log (src/Log/)

  • Logger: Writes structured logs with level filtering
  • LogRotator: Rotates, compresses, and prunes log files
  • LogFormatter: Formats log lines with optional secret sanitization
  • Cache (src/Cache/)

  • ResponseFingerprinter: Generates content hashes for ETag support
  • Doc (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 Markdown
  • HtmlGenerator: Renders docs as styled HTML
  • JsonDocGenerator: Renders docs as JSON
  • OpenRpcGenerator: Renders docs as OpenRPC 1.3.2 specification
  • Support (src/Support/)

  • RequestContext: Immutable request context (IP, headers, auth info, rawBody = transport-level body, requestBody = decoded body)
  • HookManager: Registers and fires lifecycle hooks
  • HookPoint: Enum of available hook points
  • Compressor: Gzip encode/decode utilities
  • CorrelationId: Generates unique request IDs
  • Error Handling Strategy

    All errors are converted to proper JSON-RPC error responses:

    ScenarioError CodeMessage
    Invalid JSON-32700Parse error
    Invalid request structure-32600Invalid Request
    Empty POST body-32600Invalid Request
    Empty batch []-32600Invalid Request
    Batch exceeds limit-32600Invalid Request
    Unknown method-32601Method not found
    Bad parameters-32602Invalid params
    Unknown/surplus params-32602Invalid params
    Server/application error-32603Internal error
    JSON serialization failed-32603Internal error
    Rate limit exceeded-32000Rate limit exceeded
    Authentication required-32001Authentication required
    Custom server errors-32000 to -32099Implementation-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.