olegkunitsyn/json-rpc-bundle
A Symfony bundle for building JSON-RPC 2.0 services, with request and response handling, method routing, structured error objects, and integration with the HTTP kernel and service container. Useful for exposing PHP application APIs to remote clients.
The 7.0.1 release is a patch on the 7.x line, but the headline features — batches, DTOs, and response normalization — change how you should structure API integration code, so treat 7.x as the baseline for any new work (see Gotchas for upgrading from 6.x).
Install and publish configuration first:
composer require <vendor>/<package>:^7.0
php artisan vendor:publish --tag="<package>-config" # publish config + any stubs/make commands
Where to look first, in order:
UPGRADE.md and the 7.0.0 changelog entry — 7.0.1 fixes will not cover 6→7 renames.make:data-style generator) — this is the extension point you will touch daily.pool/batch/collect entry point on the client or a dedicated builder class.First use case to prove the stack end-to-end: one endpoint, one DTO, one normalization path.
// 1. A read-only DTO per endpoint
final class OrderData extends Data // or the package's DTO base class
{
public function __construct(
public readonly int $id,
public readonly string $status,
public readonly CarbonImmutable $createdAt,
) {}
}
// 2. A single call that returns the DTO, not an array
$order = Api::orders()->show($id)->dto(OrderData::class);
Once that round-trips cleanly (including a 404 and a 500), add a batch of 3–5 ids inside a queued job. If both paths work with the same DTO and the same normalizer, your integration is correctly wired.
One DTO per response shape, not per endpoint. Share DTOs between the single-fetch and batch paths so that show() and a batched fetch of the same resource cannot drift apart. Keep DTOs readonly, give them a fromArray/from hydration entry point, and add toArray() so results can be cached, logged, or asserted in tests without touching the raw response.
Normalize once, at the boundary. Let the package's normalization layer produce the envelope (data / meta / errors) and convert it to a DTO immediately. Controllers and jobs should never see a raw response object. If you find yourself writing if (isset($json['data']['items'])) in a controller, that mapping belongs in the DTO or a custom normalizer.
Batches belong in jobs with chunking. Group batched calls by endpoint and by timeout budget, chunk to whatever the upstream provider rate-limits at, and return keyed results so partial failures can be attributed:
$results = Api::orders()->batch($ids, chunk: 50); // keyed by id
$ok = $results->successful(); // id => OrderData
$failed = $results->failed(); // id => ErrorData
Persist successes, then re-queue or dead-letter the failed subset — do not retry the whole batch. Use idempotency keys for any batched write (POST/PATCH) so a retry after a partial timeout doesn't duplicate records.
Integration points worth standardizing on day one:
new the client in a controller or a model.config/<package>.php per environment: base URL, timeout, retry/backoff, and concurrency for batches. Keep credentials in .env only.api:ping) that hits a cheap endpoint and dumps the normalized payload; useful in CI and after deploys.Http::fake()) and keep one or two recorded fixtures checked in — normalization regressions show up as DTO diffs, which are easy to review.null for optional relations, snake_case vs camelCase, and date strings all need explicit handling in the hydration method. Prefer explicit casts (CarbonImmutable::parse, backed enums) over mixed — the whole value of DTOs is that a bad payload fails loudly at the boundary.successful() / failed() explicitly.toArray(), not the raw response, or bypass normalization on replay.204 No Content, HTML error pages from a proxy, and gateway timeouts are not JSON and will produce confusing hydration errors. Guard for empty bodies and surface the status code and a truncated body in the exception message.--timeout interact; a batch that takes 90s will be killed by a 60s worker. Size chunks against the worker timeout, not the client's.How can I help you explore Laravel packages today?