psx/openrpc
psx/openrpc is a PHP library for building JSON-RPC APIs that follow the OpenRPC specification. It provides schema-driven service definitions, validation, and PSX framework integration for discoverable, self-documenting RPC endpoints.
psx/openrpc is not a framework integration — it is a small collection of PHP model (POPO) classes that mirror the OpenRPC specification (the JSON-RPC analogue to OpenAPI). You build a spec by instantiating objects in code instead of hand-writing JSON, and serialize at the end. With 3 stars and 0 dependents, treat the source as the primary documentation.
Install it and the serialization helper it relies on:
composer require psx/openrpc psx/json
Where to look first: the src/ directory. Each class (OpenRpc, Info, Method, ContentDescriptor, Server, Components, Reference, etc.) maps 1:1 to a spec object, with getters/setters for the fields. Skim OpenRpc.php and Method.php — those two drive 90% of what you'll do. There is no fluent builder, no validation layer, and no Laravel service provider; you are composing plain objects.
First use case — expose a spec for your JSON-RPC endpoints. In a Laravel app, create a dedicated class (e.g. App\Rpc\SpecBuilder) that assembles the model graph, then render it from a controller:
use PSX\OpenRPC\OpenRpc;
use PSX\OpenRPC\Info;
use PSX\OpenRPC\Method;
use PSX\OpenRPC\ContentDescriptor;
use PSX\Json\Serializer;
$spec = new OpenRpc();
$spec->setOpenrpc('1.2.6');
$spec->setInfo((new Info())->setTitle('My API')->setVersion('1.0.0'));
$method = (new Method())
->setName('user.get')
->setParams([
(new ContentDescriptor())->setName('id')->setSchema(['type' => 'integer']),
])
->setResult((new ContentDescriptor())->setName('user')->setSchema(['type' => 'object']));
$spec->setMethods([$method]);
return response(Serializer::encode($spec), 200, ['Content-Type' => 'application/json']);
That controller response is something tools like open-rpc/inspector or code generators can consume immediately.
Build one spec object per RPC namespace, then merge. Because every method is an object, you can split spec generation across domain classes (UserMethods, OrderMethods) each returning Method[], and merge them in a single SpecBuilder. This keeps the spec colocated with the handlers that implement the RPC methods.
Artisan command for spec generation. Don't generate the spec on every request — the object graph is cheap but not free. Cache the serialized JSON:
// app/Console/Commands/DumpOpenRpcSpec.php
public function handle(SpecBuilder $builder): int
{
$json = Serializer::encode($builder->build());
File::put(storage_path('app/openrpc.json'), $json);
return self::SUCCESS;
}
Then serve the file (or a cached route) rather than rebuilding it in the hot path.
Keep schemas as plain arrays, not objects. The ContentDescriptor::setSchema() in this package accepts an array (array|object depending on version) — you pass JSON-Schema-shaped PHP arrays. For anything nontrivial reuse a schema factory or the psx/schema package (same author) if you want typed schema construction too.
Reference shared types with $ref. For repeated schemas, put them under Components and reference by #/components/schemas/User. Since this package is a pure model, Reference objects are just ['$ref' => '...'] typed wrappers — build a small helper so you don't misspell JSON-Pointer paths:
function ref(string $path): Reference {
return (new Reference())->setRef('#/components/schemas/' . $path);
}
Pair with middleware for runtime symmetry. If your JSON-RPC handlers run through a Laravel middleware (e.g. rpc.guard), list them in Method::setServers() or as spec metadata so the spec documents the same constraints the code enforces.
Test the spec, not just the code. Assert the serialized output contains expected methods/params as a Pest/PHPUnit test. Because there's no validation in the library, tests are your only guard against accidentally shipping a spec that doesn't match the schema version you claim (setOpenrpc('1.2.6')).
openrpc to "banana" or leave info null. Always run the generated JSON through a JSON-Schema validator (e.g. opis/json-schema) against the OpenRPC meta-schema in CI.psx/json's Serializer, which reads public getters. Null-valued properties are typically omitted — convenient, but it means an intentional null (like params: null meaning "no params") is indistinguishable from "unset". For methods that genuinely take no params, set [] explicitly.psx/json and psx/model are built for the PSX framework. If you use psx/json's Parser to round-trip, watch out for constructor/property naming differences versus Symfony's serializer. If you'd rather not add the PSX dependency tree for just JSON encoding, json_encode($spec) often works when properties are public — but prefer Serializer to be safe with getters.setOpenrpc() value and pin the composer version; the package is old and may lag the latest spec, meaning newer fields (e.g. externalDocs, tags) may be missing from the model classes. If a field is missing, either upgrade, PR it, or drop to an array-based spec for that object — the model accepts nested arrays in most collection setters.SpecBuilder class, helper functions per object type) rather than subclassing the POPOs. Subclassing works but you inherit setter signatures that may change across PSX versions.var_export($spec) the object graph before serializing — missing setters show as null properties and reveal themselves immediately. Also confirm the content type: application/json with openrpc field present is what tooling will sniff for.Method objects per request via Serializer is measurable overhead when you could serve a cached string.How can I help you explore Laravel packages today?