yoanm/jsonrpc-server-sdk
A framework-agnostic PHP SDK for building JSON-RPC 2.0 servers: define methods, dispatch requests and notifications to handlers, and return spec-compliant responses, with middleware and flexible configuration for integrating the protocol into your app.
Install the package and read its source before wiring anything into Laravel — it is a small, low-traffic library (5 stars, no dependents), so the README plus src/ is the real documentation.
composer require yoanm/jsonrpc-server-sdk
Look at these files first, in order:
vendor/yoanm/jsonrpc-server-sdk/README.md — the canonical usage example.vendor/yoanm/jsonrpc-server-sdk/src/Server/ServerFactory.php — the entry point; confirm the exact create() signature for your installed version.vendor/yoanm/jsonrpc-server-sdk/src/Server/Server.php — handleRequest(string $request): string is the only method that matters day-to-day.The package is transport-agnostic: you give it a raw JSON-RPC request string, it gives you back a response string. HTTP is entirely your problem, which is exactly why it drops into Laravel cleanly.
A minimal endpoint:
// routes/api.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Yoanm\JsonRpcServerSdk\Server\ServerFactory;
Route::post('/rpc', function (Request $request) {
$server = ServerFactory::create([
'system.ping' => fn () => 'pong',
'math.add' => fn (array $params) => $params['a'] + $params['b'],
]);
$payload = $server->handleRequest($request->getContent());
if ($payload === '') {
return response()->noContent(); // notification-only request
}
return response($payload, 200, ['Content-Type' => 'application/json']);
});
First real use case: an internal service-to-service endpoint where a mobile app or another backend wants to call several operations through one URL, with a predictable error envelope, without building a REST surface for each one.
curl -s -X POST http://localhost/api/rpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"math.add","params":{"a":2,"b":3},"id":1}'
Move the method map out of the route closure. Put it in config/jsonrpc.php and build the server in a dedicated controller so the route stays thin:
// app/Http/Controllers/JsonRpcController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Yoanm\JsonRpcServerSdk\Server\ServerFactory;
class JsonRpcController extends Controller
{
public function __invoke(Request $request)
{
$methods = collect(config('jsonrpc.methods'))
->map(fn (string $class) => fn (array $params = []) => app($class)($params))
->all();
$payload = ServerFactory::create($methods)->handleRequest($request->getContent());
return $payload === ''
? response()->noContent()
: response($payload, 200, ['Content-Type' => 'application/json']);
}
}
Resolve handlers from the container. The big win over a hand-rolled dispatcher is that app(Handler::class) gives you constructor injection for free (repositories, HTTP clients, config). Each handler is a single __invoke(array $params): mixed class — easy to unit test and easy to autowire.
Normalize positional vs. named params early. JSON-RPC 2.0 allows "params": [1, 2] or "params": {"a": 1, "b": 2}; the SDK passes them through, so decide as a team which one you support and reject the other, or write a small adapter that maps an ordered array onto named arguments.
Handle both named and positional consistently in one place, not in every handler:
private function normalize(array $params, array $order): array
{
if (array_is_list($params)) {
return array_combine($order, $params);
}
return $params;
}
Map application exceptions onto JSON-RPC error codes inside handlers. The protocol reserves -32768 to -32000 for spec errors (-32700 parse, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal). Use application codes above that range (or the -32000..-32099 server-error band) and keep a single mapping table in config so clients can rely on it.
Use Laravel middleware for everything cross-cutting. Auth (auth:sanctum or an API-key middleware), rate limiting, and request logging all belong on the route. The SDK knows nothing about any of it, which is the point.
Log the raw body before dispatch. Because the SDK converts malformed input into a JSON-RPC error object rather than throwing, Laravel's exception reporter will never see bad requests. If you want visibility into client bugs, Log::debug('rpc', ['body' => $request->getContent()]) before handleRequest().
Notifications are a first-class case. A request without id must produce no response, and a batch of only notifications produces an empty string. The response()->noContent() branch above is not optional — returning a body breaks clients that assert on 204.
Test with raw content, not postJson() helpers alone. $this->call('POST', '/api/rpc', [], [], [], ['CONTENT_TYPE' => 'application/json'], $json) gives you exact control over the body the SDK sees. Assert on decoded JSON, not string equality, because key ordering and float formatting are not guaranteed.
200 OK with the JSON-RPC error inside the body. That is correct per spec, but it means Laravel Telescope, Sentry, and your access logs will show 200s for error traffic. Add a middleware or log channel that inspects the response envelope if you need error dashboards.$request->getContent(), not $request->all(). Re-encoding the parsed array round-trips through PHP and can change numeric types, drop duplicate keys, and mangle large integers. Always hand the SDK the raw string.[]. Guard with if ($payload === '') before returning JSON, otherwise you emit an invalid Content-Type: application/json empty body.ValidationException inside a handler, it becomes an HTML 422 instead of a JSON-RPC -32602. Catch and translate at the handler boundary, or register a renderable callback that produces a JSON-RPC error envelope when the request path is /api/rpc.Validator and map failures to -32602, or pull in a companion validator package from the same author (yoanm/jsonrpc-params-validator). Decide explicitly; silent null params are a common source of confusing client errors.response()->json(). If you post-process the string, use JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE consistently, and never json_decode + json_encode the SDK output just to "clean it up".Server instance as a singleton in a service provider, the method map (and any captured state) is frozen for the worker's lifetime. Bind it as a scoped binding, or build it per request as in the example."yoanm/jsonrpc-server-sdk": "^<installed-major>" and read the changelog before upgrading.App\JsonRpc\Dispatcher with one dispatch(string $payload): string method and let the controller depend on that. It costs twenty lines and means swapping the SDK for a hand-rolled dispatcher — or adding an HTTP/PSR-7 adapter from the same ecosystem — is a one-file change rather than a rewrite.ServerFactory for the exact option array on your version. If you need per-method middleware (e.g. logging only admin.* methods), implement it as decorators around your handler classes rather than fighting the SDK's dispatch loop.How can I help you explore Laravel packages today?