alcedo-rml/json-rpc-server-bundle

Symfony bundle for Alcedo JSON-RPC server

Maintainers

Package info

github.com/reactive-mnemonic-layers/json-rpc-server-bundle

pkg:composer/alcedo-rml/json-rpc-server-bundle

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.1 2026-08-12 09:48 UTC

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 Server service resolves procedures through Alcedo\Rml\JsonRpcServerBundle\ProceduresSecureCollection, a decorator around Alcedo\Rml\JsonRpc\ProceduresCollection that checks access before handing back the callable. A request for a procedure that exists but is denied gets the same ProcedureNotFoundException as 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 — exposes test.echo, returning whatever parameters it receives.
  • MathRemoteService — exposes test.math.add / test.math.sub (and similar arithmetic operations), registered via tags rather than attributes.
  • ThrowExceptionService — exposes test.throw_exception, which always throws, to verify that exceptions are correctly converted into JSON-RPC error responses.
  • ListProceduresService — exposes procedures.list (see Listing procedures).

Running the tests

composer install
vendor/bin/phpunit