API documentation
You don't need to write documentation separately: the bundle builds it from the code, i.e. PHP types, PHPDoc, DTOs, enums, validation constraints and attributes.
| Request | Response |
|---|---|
GET /rpc |
OpenRPC document |
GET /rpc?info |
Only the document header (info, servers), without methods and schemas |
GET /rpc/postman |
Postman collection |
GET /rpc/v2, GET /rpc/v2/postman |
The same for an API version |
GET /rpc/internal, … |
The same for an API group |
If protected_doc: true is set in the configuration, these requests also require a token.
What goes into OpenRPC
- Methods are grouped by tags using the class alias from
#[RPC\Info]; the full class name is in the tag description. - Parameters and result are JSON Schema built from native types and PHPDoc: union, nullable,
Dto[],array<string, Dto>; default values and whether a parameter is required. - DTOs and enums are moved to
#/components/schemasand referenced via$ref, so each is described once. - Constraints from
#[RPC\Assertions]are converted into schema rules. - Errors of a method come from
@throwstags in the docblock (the code and message of each exception). - Deprecated methods (
@deprecated,#[\Deprecated]) are marked with"deprecated": true. - Versions are listed in
info.x-versions; UFO service information is in thex-ufoextensions.
/**
* Returns a user by id
* @param int $id User identifier
* @return UserDto
* @throws UserNotFoundException
*/
public function get(int $id): UserDto
Postman collection
GET /rpc/postman returns a collection with all methods and sample parameters. The collection name is RPC: <project_name> <version>, for a group with the [group] suffix. The server address is in the base_url variable, the token is in the header named by token_name.
Translating method names and descriptions
A method's name (summary in OpenRPC) and description can be set and translated without changing the code. Translations go into translations/rpc.<locale>.yaml (or .yml), the key is the RPC method name:
# translations/rpc.uk.yaml
catalog.list:
title: Каталог
description: Список товарів
# translations/rpc.en.yaml
catalog.list:
title: Catalog
description: List of products
The language is chosen from the Accept-Language header among the languages that have translation files (uk-UA gives uk); if nothing matches, kernel.default_locale is used. The response gets Content-Language and Vary: Accept-Language.
Translations for an API group
Overrides for a single group go into the rpc/ subdirectory:
translations/rpc.uk.yaml # base, for all groups
translations/rpc/bfa.uk.yaml # only for the bfa group
# translations/rpc/bfa.uk.yaml
catalog.list:
title: Каталог для партнерів # description is taken from the base translation
Each field is looked up in the following order (group takes precedence over language):
| Step | Source |
|---|---|
| 1 | group, client language |
| 2 | group, kernel.default_locale |
| 3 | base, client language |
| 4 | base, kernel.default_locale |
| 5 | #[RPC\Info] / PHPDoc |
The language is the last segment of the file name, the group is everything before it: rpc/partner.v2.en.yaml belongs to the partner.v2 group.
Don't name a group after a Symfony Translator domain (messages, validators, security or a custom one): the Translator scans translations/ recursively and will treat rpc/messages.uk.yaml as part of the messages domain.
The list of languages is shared by all groups; see Open issues.
Translations from extensions
A bundle that adds RPC methods can ship their translations. Additional directories with the same structure (rpc.<locale>.yaml, rpc/<group>.<locale>.yaml) are registered via translations.paths:
ufo_json_rpc:
translations:
paths:
- '%kernel.project_dir%/vendor/acme/rpc-extension/translations'
or from the extension itself in prepend():
public function prepend(ContainerBuilder $container): void
{
$container->prependExtensionConfig('ufo_json_rpc', [
'translations' => ['paths' => [__DIR__.'/../../translations']],
]);
}
Directories are merged field by field, a later one overrides an earlier one; the application's translations/ always comes last. Translations are collected during container compilation; in debug mode the container is rebuilt when the files change.
RpcLocaleNegotiator
Extensions with their own translations can use the language selection service that the documentation uses:
use Ufo\JsonRpcBundle\Server\RpcLocaleNegotiator;
public function __construct(private RpcLocaleNegotiator $localeNegotiator) {}
$locale = $this->localeNegotiator->negotiate($request, ['en', 'uk']); // null if the list is empty
$this->localeNegotiator->applyLocale($request); // after LocaleListener (priority 16)
$this->localeNegotiator->addLanguageHeaders($request, $response); // Content-Language + Vary
Documentation caching
Outside debug mode the generated document is cached and served with an ETag. A client that sends If-None-Match gets 304 Not Modified without regeneration. The cache key takes into account the route, version, ?info, group, host, language, the set of methods and container.build_id, so the cache is refreshed on any change to the methods or container rebuild. Nothing needs to be configured; the pool is set by cache.pool.