alcedo-rml / json-rpc-server-bundle
Symfony bundle for Alcedo JSON-RPC server
Package info
github.com/reactive-mnemonic-layers/json-rpc-server-bundle
pkg:composer/alcedo-rml/json-rpc-server-bundle
Requires
- php: >=8.2
- alcedo-rml/json-rpc-server: ^2
- psr/container: ^2.0
- symfony/dependency-injection: ^8.0
- symfony/framework-bundle: ^8.0
- symfony/http-foundation: ^8.0
- symfony/http-kernel: ^8.0
- symfony/security-core: ^8.1
- symfony/yaml: ^8.0
Requires (Dev)
- phpunit/phpunit: ^11.0 || ^12.0
This package is auto-updated.
Last update: 2026-08-12 09:56:27 UTC
README
A Symfony bundle that exposes services as JSON-RPC 2.0
remote procedures, on top of the alcedo-rml/json-rpc-server
library. It discovers remote procedures from your service definitions at container compile time,
enforces per-procedure access control through Symfony Security, and serves everything through a
ready-to-use HTTP controller.
Requirements
- PHP 8.2+
- Symfony 8.0 (
dependency-injection,framework-bundle,http-foundation,http-kernel,yaml,security-core)
Installation
composer require alcedo-rml/json-rpc-server-bundle
If Symfony Flex doesn't register the bundle automatically, add it to config/bundles.php:
return [ // ... Alcedo\Rml\JsonRpcServerBundle\JsonRpcServerBundle::class => ['all' => true], ];
Routing
The bundle ships a single controller, Alcedo\Rml\JsonRpcServerBundle\Controller\JsonRpcServerController,
that accepts a JSON-RPC request (or batch) as a JSON POST body and returns a JSON-RPC response.
Wire it to a route, for example in config/routes.yaml:
alcedo_jsonrpc_server: path: /jsonrpc controller: Alcedo\Rml\JsonRpcServerBundle\Controller\JsonRpcServerController methods: [POST] condition: "request.headers.get('Content-Type') == 'application/json'"
Exposing remote procedures
There are two ways to expose a service method as a JSON-RPC procedure: PHP attributes, or service tags.
Attributes
Mark the service class with #[AsRemoteService] and each method you want to expose with
#[AsRemoteProcedure(procedure: '...')]. Parameters can optionally carry
#[AsRemoteProcedureParameter] for a description used in the discovered metadata.
use Alcedo\Rml\JsonRpcServerBundle\Attribute\AsRemoteProcedure; use Alcedo\Rml\JsonRpcServerBundle\Attribute\AsRemoteProcedureParameter; use Alcedo\Rml\JsonRpcServerBundle\Attribute\AsRemoteService; #[AsRemoteService(name: 'Echo Service', description: 'Just echoing back its input')] class EchoService { #[AsRemoteProcedure(procedure: 'test.echo', description: 'Echo back its parameters')] public function call( #[AsRemoteProcedureParameter(description: 'The server-sent parameters')] ...$input ): array { return $input; } }
A class can also be invokable and exposed as a single procedure, by placing
#[AsRemoteProcedure] on the class itself:
use Alcedo\Rml\JsonRpcServerBundle\Attribute\AsRemoteProcedure; use Alcedo\Rml\JsonRpcServerBundle\Attribute\AsRemoteService; #[AsRemoteService] #[AsRemoteProcedure(procedure: 'test.throw_exception', description: 'Always throws an exception')] class ThrowExceptionService { public function __invoke(): never { throw new \RuntimeException('This service will always throw an exception.'); } }
As long as autoconfiguration is on (it is, by default, for any service defined under the
bundle's own src/, and typically for your application's src/ too), no extra service
configuration is required — the #[AsRemoteService] attribute is enough to trigger discovery.
Service tags
Alternatively, describe the procedure directly on the service definition with the
alcedo_rml_json_rpc_server.remote_service tag. This is useful when you don't want to (or
can't) add attributes to the class, or want to expose the same method under several procedure
names:
services: App\Service\MathRemoteService: tags: - name: !php/const Alcedo\Rml\JsonRpcServerBundle\JsonRpcServerBundle::REMOTE_SERVICE_TAG procedure: 'test.math.add' method: 'add' description: 'Adds its two parameters' type: 'int' params: - name: 'a' description: 'First operand' type: 'int' required: true - name: 'b' description: 'Second operand' type: 'int' required: true
If method is omitted, the tagged class must be invokable (__invoke). If params is
omitted, parameter metadata is inferred by reflecting over the method's signature instead.
Access control
Any procedure — attribute- or tag-based — can be restricted with an access option, checked
against Symfony's AuthorizationCheckerInterface::isGranted() (a role, an expression, a voter
attribute, anything isGranted() accepts). A procedure without access (the default, null)
is open to anyone who can reach the endpoint.
#[AsRemoteProcedure(procedure: 'admin.users.delete', description: 'Deletes a user', access: 'ROLE_ADMIN')] public function delete(int $id): void { // ... }
services: App\Service\AdminService: tags: - name: !php/const Alcedo\Rml\JsonRpcServerBundle\JsonRpcServerBundle::REMOTE_SERVICE_TAG procedure: 'admin.users.delete' method: 'delete' access: 'ROLE_ADMIN'
This is enforced in two places:
- Calling a procedure: the
Serverservice resolves procedures throughAlcedo\Rml\JsonRpcServerBundle\ProceduresSecureCollection, a decorator aroundAlcedo\Rml\JsonRpc\ProceduresCollectionthat checksaccessbefore handing back the callable. A request for a procedure that exists but is denied gets the sameProcedureNotFoundExceptionas one that doesn't exist at all — access is not leaked by distinguishing "not found" from "forbidden". - Listing procedures:
procedures.list(see below) only returns procedures the caller is currently granted access to, so introspection never advertises procedures a client can't call.
Making a request
Once a route is wired up, send a standard JSON-RPC 2.0 POST request:
curl -X POST http://localhost/jsonrpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"test.echo","params":{"hello":"world"},"id":1}'
{"jsonrpc":"2.0","id":1,"result":{"hello":"world"}}
Batches (an array of requests) and notifications (a request without an id) are supported, as
defined by the JSON-RPC 2.0 specification. Errors — unknown procedures, invalid requests, or
exceptions thrown by a procedure — are reported as JSON-RPC error objects rather than HTTP error
statuses.
How discovery works
At container compile time, RemoteProceduresDiscoveryPass scans every registered service
definition. For each one, it either reads procedure metadata off the
alcedo_rml_json_rpc_server.remote_service tag, or — if the service's class carries
#[AsRemoteService] — reflects over its public methods (and the class itself, if invokable)
for #[AsRemoteProcedure] attributes. The resulting procedure-to-service map is published as a
container parameter and consumed by Alcedo\Rml\JsonRpc\ProceduresCollection to resolve and
call procedures lazily, by service id, when a request comes in; descriptive metadata (including
access) for every discovered procedure is published alongside it and consumed by
ProceduresSecureCollection and procedures.list (see Access control).
Listing procedures
The bundle exposes procedures.list, backed by Alcedo\Rml\JsonRpcServerBundle\Service\ListProceduresService,
as a live introspection endpoint: it returns the discovered metadata (procedure name,
description, return type, parameters, access) for every procedure the calling client is
currently granted access to.
curl -X POST http://localhost/jsonrpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"procedures.list","id":1}'
Bundled example services
The bundle ships example services under Alcedo\Rml\JsonRpcServerBundle\Service, useful for
manually exercising the JSON-RPC pipeline. Only MathRemoteService and ListProceduresService
are meant to be registered unconditionally; the others are intended for the dev environment —
check config/services.yaml in your application before assuming one is available everywhere:
EchoService— exposestest.echo, returning whatever parameters it receives.MathRemoteService— exposestest.math.add/test.math.sub(and similar arithmetic operations), registered via tags rather than attributes.ThrowExceptionService— exposestest.throw_exception, which always throws, to verify that exceptions are correctly converted into JSON-RPC error responses.ListProceduresService— exposesprocedures.list(see Listing procedures).
Running the tests
composer install vendor/bin/phpunit