yoanm/symfony-jsonrpc-http-server
Symfony integration for yoanm/jsonrpc-server that exposes JSON-RPC 2.0 APIs over HTTP. Configure the server, register method handlers as Symfony services, and handle JSON-RPC requests through the framework.
This package is the HTTP transport layer of the yoanm JSON-RPC stack: it takes an HTTP request whose body is a JSON-RPC 2.0 payload, hands it to the core server SDK (yoanm/jsonrpc-server-sdk), and turns the resulting JSON-RPC response back into a PSR-7 HTTP response. It does not define your methods, validate params, or authenticate anything — that is the job of your handlers and the sibling packages (yoanm/symfony-jsonrpc-params-validator, etc.).
Minimal path:
composer require yoanm/symfony-jsonrpc-http-server
# you also need a PSR-7 / PSR-17 implementation
composer require nyholm/psr7
Register the bundle manually (there is no Flex recipe for a package this small):
// config/bundles.php
Yoanm\SymfonyJsonRpcHttpServerBundle\JsonRpcHttpServerBundle::class => ['all' => true],
Where to look first, in this order:
vendor/yoanm/symfony-jsonrpc-http-server/src/DependencyInjection/Configuration.php — the real config tree. Key names change between minor versions; read this file rather than trusting blog posts.vendor/yoanm/jsonrpc-server-sdk/src/ — the Server, method resolution, and error formatting live here, not in this package.bin/console debug:router | grep -i json tells you the endpoint path immediately.First use case — a single POST /json-rpc endpoint:
# config/packages/json_rpc_http_server.yaml (verify key names against Configuration.php)
json_rpc_http_server:
method:
'ping':
service: App\JsonRpc\PingMethod
final class PingMethod
{
public function __invoke(?array $params = null): string
{
return 'pong';
}
}
curl -s -X POST http://localhost:8000/json-rpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"ping","id":1}'
# {"jsonrpc":"2.0","result":"pong","id":1}
If that round-trips, everything else is just adding handler services and mappings.
One invokable service per method. This is the pattern the SDK is built around and it scales best: App\JsonRpc\User\GetMethod, App\JsonRpc\User\CreateMethod. The service id is what you map in config, so the config file becomes a readable API surface / method registry. Keep the class names mirrored in the JSON-RPC method names (user.get → User\GetMethod) and code review of the config is trivial.
Result vs error. Return a plain value (or a serializable DTO) for success — the transport wraps it in {"result": ...}. To return a JSON-RPC error rather than a PHP failure, throw the SDK's JSON-RPC exception with a code and optional data:
throw new JsonRpcException(-32001, 'User not found', ['userId' => $id]);
A plain \RuntimeException typically collapses into the generic -32603 / Internal error, which is what you want for bugs and not for expected domain failures. Decide the convention early: domain exceptions → explicit codes; everything else → internal error.
Validation is a separate concern. Do not hand-roll param checks in the handler. Add yoanm/symfony-jsonrpc-params-validator and declare a schema per method (Symfony Validator constraints on a DTO work well). The invalid-params error (-32602) is then produced by the stack, consistently, before your handler runs — which also keeps your handlers free of defensive isset() noise.
Auth and rate limiting belong in Symfony, not JSON-RPC. Because the entire API is one POST endpoint, a firewall rule on that path, a kernel.request subscriber, or a RequestMatcher guards every method at once. Per-method authorization goes either in the handler or in a decorator around the method resolver; the latter is cleaner if you have many methods and want a single policy hook.
Decorators for cross-cutting concerns. The server is a service, so:
#[AsDecorator(decorates: 'yoanm.jsonrpc.server')] // check the actual service id
final class LoggingServer implements ServerInterface { /* ... */ }
is the idiomatic way to add timing, request logging, metrics, or a correlation id without touching handlers. Decorate the method-resolver/handler service if you want per-method instrumentation instead.
Batch and notifications are free. Send a JSON array as the body to exercise batch mode; send a payload without an id to test notifications. Test both explicitly in your functional suite — clients rely on them and they are the two places where naive implementations break.
Testing. Unit-test handlers directly (new PingMethod()(...)) — they are plain PHP. For end-to-end, a KernelBrowser POST with a JSON body is enough; assert on decoded result/error keys, not on HTTP status (see gotchas). A small collection of .http/curl fixtures checked into the repo pays for itself when upgrading.
Keep the transport thin. If you find yourself reaching into PSR-7 inside a handler, you are fighting the design. Handlers get params, return data, throw domain exceptions. Anything else belongs in a decorator or a subscriber.
error.code), not the status line. Tests asserting 404 for a missing method or 400 for bad params will fail. Assert on the payload.id. JSON-RPC requires echoing the id exactly; a client sending "id": "abc" must get "abc" back, and 0 is a valid id. If you normalize ids anywhere in a proxy or middleware, clients will silently drop responses.symfony/psr-http-message-bridge plus a PSR-17 factory). The classic failure is Unable to create PSR-7 request — that means no PSR-17 implementation is registered, not that your code is wrong.application/json-rpc or no Content-Type can be rejected or silently mishandled. Be liberal in what you accept if you have heterogeneous clients, and document the expected header.json_decodes the body will blow up; handle the empty case.cache:clear/container compile time, not at runtime. That is a feature — but it also means bin/console cache:clear after editing the mapping, and it means the handler services must be resolvable by the container (don't map a service id that is defined lazily behind a factory you forgot to register).error.data. In debug mode exception messages/traces are helpful; in production they are an information disclosure. Wire an explicit error formatter/mapper and test the production configuration, not just dev.OPTIONS requests and a strict Access-Control-Allow-Origin/-Headers. nelmio/cors-bundle with a path rule is usually less painful than hand-writing a listener.params. Redact before logging; log the method name and id, not the whole envelope, unless you have a debug flag.composer.json, read composer.json constraints against your Symfony version before upgrading, and be prepared to fork or wrap. The upside is that the whole transport is a few hundred lines and the extension points (decorating the server, overriding the error formatter, custom method resolvers) are shallow.kernel.response listener only as a last resort (it will see JSON-RPC responses and can easily corrupt them).How can I help you explore Laravel packages today?