API groups

← API groups

Ashterix

docs: rewrite JsonRpcBundle docs, add SDK docs, move them under docs/api

en/docs/api/json-rpc-bundle/functionality/groups.md new file +132 −0

API groups

Available since version 10.2. Applications without groups work as before: everything belongs to the default general group.

Groups let you have several RPC APIs in one application — for example, a public one for clients, an internal one for the admin panel and separate BFFs. Each group has its own address, set of methods, versions, documentation and, if needed, its own token validation.

Entry points

Import the bundle routes once for each API. prefix sets the address, and defaults.api_group sets the group (without it — general):

# config/routes/ufo_json_rpc.yaml
ufo_json_rpc:
    resource: ../../vendor/ufo-tech/json-rpc-bundle/config/router.yaml
    prefix: /rpc
    trailing_slash_on_root: false

ufo_json_rpc_internal:
    resource: ../../vendor/ufo-tech/json-rpc-bundle/config/router.yaml
    prefix: /rpc/internal
    defaults:
        api_group: internal
    trailing_slash_on_root: false

ufo_json_rpc_public:
    resource: ../../vendor/ufo-tech/json-rpc-bundle/config/router.yaml
    prefix: /rpc/public
    defaults:
        api_group: public
    trailing_slash_on_root: false

POST /rpc/internal executes a request, GET /rpc/internal returns OpenRPC, GET /rpc/internal/postman returns Postman; versioned addresses are /rpc/internal/v2 and so on. A group's documentation contains only its own methods and versions.

Route names get the group prefix: general_ufo_rpc_api_server, internal_ufo_rpc_api_server, … The old names without a prefix remain aliases for general, so router->generate('ufo_rpc_api_server') works as before.

#[RPC\APIGroups]

The attribute on a class or method defines which groups the method belongs to:

public function __construct(array $groups = [], bool $withDefault = true)

withDefault: true adds the general group, false does not.

use Ufo\JsonRpcBundle\ApiMethod\Interfaces\IRpcService;
use Ufo\RpcObject\RPC;

#[RPC\Info('catalog')]
#[RPC\APIGroups(['internal', 'public'], false)]
class CatalogApi implements IRpcService
{
    public function list(): array { /* ... */ }                  // internal and public

    #[RPC\APIGroups(['internal'], false)]
    public function updatePrice(int $productId, int $price): true { return true; } // internal only

    #[RPC\APIGroups(['public'], false)]
    public function featured(): array { return [1]; }             // public only

    #[RPC\APIGroups()]
    public function status(): string { return 'ok'; }             // general only

    #[RPC\APIGroups(['internal'], false)]
    #[RPC\IgnoreApi]
    public function rebuildIndex(): true { return true; }         // not published anywhere
}
Method /rpc (general) /rpc/internal /rpc/public
catalog.list — yes yes
catalog.updatePrice — yes —
catalog.featured — — yes
catalog.status yes — —
catalog.rebuildIndex — — —

Rules:

  • a method attribute fully replaces the class groups; without attributes a method belongs to general;
  • APIGroups([], false) does not publish the method in any group; #[RPC\IgnoreApi] takes precedence over groups;
  • general is not "all groups" but an ordinary default group;
  • method maps and versions are separated by group: a method from another group returns Method not found;
  • for HTTP the group is determined by the route, not by a field in the JSON.

Tokens for groups

Without additional configuration, all groups are checked by the base validator (security.clients_tokens or your ITokenValidator). Separate validation for groups is set with security.providers. Each provider has exactly one of the service or clients_tokens parameters and a non-empty api_groups.

ufo_json_rpc:
    security:
        protected_api: true
        token_name: 'Ufo-RPC-Token'
        clients_tokens:
            - '%env(resolve:UFO_API_TOKEN)%'        # general and groups without a provider
        providers:
            internal:
                api_groups: [internal]
                clients_tokens:
                    - '%env(resolve:UFO_INTERNAL_API_TOKEN)%'
            public:
                service: App\Security\UserTokenValidator   # implements ITokenValidator
                api_groups: [public]
  • A provider replaces the base validation for its groups; if it rejects a token, the base validation is not run.
  • Two providers for the same group, a missing service or a class without ITokenValidator cause a container compilation error.
  • protected_api, protected_doc and token_name are shared by all groups; protected documentation is checked by the provider of its group.

Console and async

bin/console ufo:rpc:process '{"jsonrpc":"2.0","id":1,"method":"catalog.updatePrice","params":{"productId":1,"price":100}}' \
    --api-group=internal --ver=v1

Batch child processes keep the group and version of the route. For Symfony Messenger the group is passed explicitly:

$message = new \Ufo\RpcObject\RpcAsyncRequest($request, $token, apiGroup: 'internal');

Messages without a group are executed in general.

Translations for a group

Method names and descriptions in the documentation can be overridden for a specific group with translations/rpc/<group>.<locale>.yaml files. See API documentation.