JSON-RPC Request Builder — PHP source
Build valid JSON-RPC 2.0 requests, notifications, success responses, and error responses, plus batch arrays. Validate message structure.
This is the PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.
<?php
/**
* json-rpc-builder - polyglot showcase port (PHP).
*
* Pure JSON-RPC 2.0 message builder for the JSON-RPC Builder tool on CosmoDev
* (dev.cosmolabs.org). Ported from the canonical TypeScript source at
* src/lib/jsonRpc.ts so the tool page can display the same logic across six
* languages.
*
* Language: PHP 7.4+ (standard library only - no Composer packages).
* Display source - part of CosmoDev's polyglot tool pages.
*
* Behavior is identical to the TS lib: same inputs -> same outputs. Builds
* requests, notifications, success/error responses, and batches. Never throws;
* every builder returns an Outcome array.
*/
namespace CosmoDev\JsonRpc;
// Protocol version emitted on every message.
const JSONRPC_VERSION = '2.0';
/**
* Pre-defined messages for the error codes reserved by the JSON-RPC 2.0 spec
* (https://www.jsonrpc.org/specification#error_object, section 5.1). The
* -32000..-32099 band is reserved for server-defined "Server error" values.
*/
const STANDARD_ERRORS = [
-32700 => ['message' => 'Parse error'],
-32600 => ['message' => 'Invalid Request'],
-32601 => ['message' => 'Method not found'],
-32602 => ['message' => 'Invalid params'],
-32603 => ['message' => 'Internal error'],
-32000 => ['message' => 'Server error'],
];
/**
* Build the canonical return shape: ok flag, serialized JSON, error reason.
* Mirrors the TS Outcome one-for-one so the UI consumes both ports identically.
*/
function outcome(bool $ok, string $json, ?string $error): array
{
return ['ok' => $ok, 'json' => $json, 'error' => $error];
}
function is_non_empty_string($value): bool
{
return is_string($value) && strlen($value) > 0;
}
/**
* Serialize a value to JSON with a TS-compatible indent policy.
*
* PHP's json_encode produces compact output by default and multi-line output
* with JSON_PRETTY_PRINT (4-space indent). To match the TS lib:
* - indent <= 0 -> compact, single line.
* - indent > 0 -> pretty (PHP uses its fixed 4-space width; the parsed
* result is identical to the TS output, only the whitespace differs).
*/
function safe_stringify($value, int $indent): string
{
$flags = 0;
if ($indent > 0) {
$flags = JSON_PRETTY_PRINT;
}
// JSON_UNESCAPED_SLASHES keeps "/" readable, matching JS JSON.stringify
// (which never escapes "/"); JSON_UNESCAPED_UNICODE keeps multibyte text
// human-readable in the showcase.
$flags |= JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
$json = json_encode($value, $flags);
// json_encode only returns false on broken UTF-8 / recursion, neither of
// which occurs for these controlled shapes; fall back to a safe value.
return $json === false ? '' : $json;
}
/**
* Build a JSON-RPC 2.0 Request.
*
* A request carries an id that the server echoes back in its response, making
* it a synchronous ask/reply pair (unlike a notification). The id may be a
* string, int, or null.
*
* Note on optional params: PHP cannot distinguish "argument omitted" from
* "argument is null" without func_num_args gymnastics. The idiomatic PHP
* convention here is: null params = omitted (no `params` field emitted). This
* matches the overwhelming majority of real call sites; the exotic
* explicitly-null-params case is intentionally collapsed.
*
* @param mixed $method remote method name
* @param mixed $params structured params, or null to omit
* @param int|float|string|null $id correlation id (default 1)
* @param int $indent 0 = compact, N = pretty with N-space indent
*/
function build_request($method, $params = null, $id = 1, int $indent = 0): array
{
if (!is_non_empty_string($method)) {
return outcome(false, '', 'method must be a non-empty string');
}
$obj = ['jsonrpc' => JSONRPC_VERSION, 'method' => $method];
if ($params !== null) {
$obj['params'] = $params;
}
$obj['id'] = $id;
return outcome(true, safe_stringify($obj, $indent), null);
}
/**
* Build a JSON-RPC 2.0 Notification: fire-and-forget, no id, server MUST NOT
* reply. See build_request for the params-null convention.
*/
function build_notification($method, $params = null, int $indent = 0): array
{
if (!is_non_empty_string($method)) {
return outcome(false, '', 'method must be a non-empty string');
}
$obj = ['jsonrpc' => JSONRPC_VERSION, 'method' => $method];
if ($params !== null) {
$obj['params'] = $params;
}
return outcome(true, safe_stringify($obj, $indent), null);
}
/**
* Build a JSON-RPC 2.0 success Response. The id echoes the request id this
* answers; the result is always present (even if null).
*/
function build_success_response($id, $result, int $indent = 0): array
{
$obj = ['jsonrpc' => JSONRPC_VERSION, 'result' => $result, 'id' => $id];
return outcome(true, safe_stringify($obj, $indent), null);
}
/**
* Build a JSON-RPC 2.0 error Response.
*
* The message falls back through a chain: explicit argument -> the standard
* text for the given code -> the generic word "Error". `data` carries
* optional structured detail (null means "no data field").
*/
function build_error_response($id, int $code, ?string $message = null, $data = null, int $indent = 0): array
{
if ($message !== null && $message !== '') {
$msg = $message;
} elseif (isset(STANDARD_ERRORS[$code])) {
$msg = STANDARD_ERRORS[$code]['message'];
} else {
$msg = 'Error';
}
$error = ['code' => $code, 'message' => $msg];
if ($data !== null) {
$error['data'] = $data;
}
$obj = ['jsonrpc' => JSONRPC_VERSION, 'error' => $error, 'id' => $id];
return outcome(true, safe_stringify($obj, $indent), null);
}
/**
* Serialize a batch of pre-built JSON-RPC messages.
*
* Per spec section 6, a batch is a non-empty array of messages exchanged in a
* single round-trip. Callers supply already-built message objects (not
* strings); we only wrap and serialize them.
*/
function build_batch($messages, int $indent = 0): array
{
if (!is_array($messages) || count($messages) === 0) {
return outcome(false, '', 'batch must be a non-empty array');
}
return outcome(true, safe_stringify(array_values($messages), $indent), null);
}
/**
* Lightweight structural check for a JSON-RPC 2.0 message.
*
* Permissive about field VALUES (it does not recurse into params/error data);
* it only verifies the message "shape" so a UI can surface what is wrong in
* plain English. Returns every problem found, not just the first.
*
* The expected input is an associative array (json_decode's representation of
* a JSON object). In JS every `typeof === "object"` value passes the first
* gate (incl. arrays); the PHP port narrows that to associative arrays - the
* idiomatic decoded-JSON shape.
*
* @return array{valid: bool, errors: string[]}
*/
function validate_rpc($obj): array
{
if (!is_array($obj)) {
return ['valid' => false, 'errors' => ['Not an object.']];
}
$errors = [];
if (($obj['jsonrpc'] ?? null) !== JSONRPC_VERSION) {
$errors[] = 'jsonrpc must be "2.0".';
}
if (array_key_exists('method', $obj) && !is_string($obj['method'])) {
$errors[] = 'method must be a string.';
}
$hasResult = array_key_exists('result', $obj);
$hasError = array_key_exists('error', $obj);
if ($hasResult && $hasError) {
$errors[] = 'cannot have both result and error.';
}
// A well-formed message is exactly one of: request/notification (method),
// success response (result), or error response (error).
$hasMethod = array_key_exists('method', $obj);
if (!$hasMethod && !$hasResult && !$hasError) {
$errors[] = 'must have method, result, or error.';
}
return ['valid' => count($errors) === 0, 'errors' => $errors];
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →