09 — OpenRPC
OpenRPC — для JSON-RPC то, чем OpenAPI для REST: machine-readable контракт, который потребляют SDK-генераторы, doc-renderer’ы и mocking-серверы.
Бандл эмитит валидный OpenRPC 1.3.2 документ для всех зарегистрированных методов:
bin/console debug:rpc --openrpc \
--title="Billing API" \
--api-version="2.4.0" \
> openrpc.json
Что эмитится
{
"openrpc": "1.3.2",
"info": {
"title": "Billing API",
"version": "2.4.0"
},
"methods": [
{
"name": "user.get",
"description": "Найти пользователя по email.",
"params": [
{
"name": "email",
"required": true,
"schema": {
"type": "string",
"format": "email"
}
},
{
"name": "limit",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 100
}
}
],
"result": {
"name": "user.get_result",
"schema": {
"type": "object",
"properties": { ... },
"additionalProperties": false
}
},
"x-rpc-roles": ["ROLE_USER"],
"x-rpc-roles-match": "any"
},
{
"name": "user.legacy_get",
"deprecated": true,
"x-deprecation-reason": "Use user.get instead.",
...
}
]
}
Flattening параметров
OpenRPC params строятся из той же прекомпилированной JSON Schema, что и MCP
inputSchema (на этапе сборки контейнера). Каждый business-ключ плоской
схемы становится отдельным OpenRPC-параметром.
Покрывает:
- Один DTO — поля конструктора (
email,limit, …). - DTO + scalar рядом — поля DTO и
#[Rpc\Param]/ auto-promoted scalars (street,city,autoId). - Только scalars — один entry на параметр.
Server-side (Context, Request, RpcRequest) не попадают в контракт.
Два DTO с общим top-level JSON-ключом отклоняются при сборке контейнера (как и при runtime resolution).
Result schema
result.schema берётся в таком порядке приоритета (та же логика, что у MCP
outputSchema — контракты не расходятся):
#[Rpc\Method(outputSchema: SomeDto::class)]— схематизируется черезJsonSchemaBuilder.#[Rpc\Method(outputSchema: [...])]— литеральный JSON Schema, отдаётся как есть.- Тип возврата
__invoke()— scalar →{type: …}; class/enum →JsonSchemaBuilder::fromClass(…);array→{type: 'array'}. mixed/void/ нет return type →schema: {}(OpenRPC требует ключ, но потребитель видит «форма не объявлена»).
Схема только подсказка — против реального ответа не валидируется (см. обоснование в MCP § Output schema).
Кастомные x- расширения
Бандл эмитит некоторые non-standard поля в x- namespace. OpenRPC клиенты
обязаны игнорировать unknown x- ключи; bundle-aware тулинг (ваш собственный
SDK-генератор, docs renderer) может их читать:
| Поле | Тип | Источник |
|---|---|---|
x-deprecation-reason |
string | #[Rpc\Method(deprecated: '...')] |
x-rpc-roles |
string[] | #[Rpc\Method(roles: [...])] |
x-rpc-roles-match |
"any" | "all" |
#[Rpc\Method(rolesMatch: ...)] |
x-rpc-streaming |
boolean | #[Rpc\Stream] |
x-rpc-stream-format |
string | #[Rpc\Stream(format: ...)] |
При json_rpc_server.security.expose_role_names: false документ не содержит
x-rpc-roles / x-rpc-roles-match — role identifier’ы остаются внутренними и
здесь, как в сообщениях AccessDenied и в записях MCP tools.
Интеграция с SDK-генераторами
TypeScript
@open-rpc/generator эмитит типизированные
client SDK:
npx @open-rpc/generator generate \
-t client-typescript \
--openrpcDocument=openrpc.json \
-o ./sdk-ts
Результат: типизированный TS-клиент, где client.user.get({ email }) возвращает
типизированный результат.
Python
npx @open-rpc/generator generate \
-t client-python \
--openrpcDocument=openrpc.json \
-o ./sdk-py
Документация
@open-rpc/docs-react или
OpenRPC playground рендерят документ в
browsable docs.
CI интеграция
Генерируйте документ в CI, чтобы ловить непреднамеренные изменения формы API:
# .github/workflows/openrpc.yml
- run: bin/console debug:rpc --openrpc > current-openrpc.json
- run: diff openrpc.json current-openrpc.json # fail at drift
Или коммитьте под docs/openrpc.json и требуйте чтобы оставался в синке.
Стратегии версионирования
OpenRPC имеет info.version для всего документа. Позиция бандла: имя
JSON-RPC метода — это string namespace; версионируйте через имя.
Стратегия A: префикс на версию
#[Rpc\Method('v1.user.get')]
#[Rpc\Method('v2.user.get')]
Генерируйте один OpenRPC документ покрывающий все версии, или фильтруйте и
эмитьте per-version (нужен кастомный код поверх OpenRpcDocumentBuilder).
Стратегия B: отдельные routes
json_rpc_server:
routes:
rpc: '/rpc/v2' # плюс второй экземпляр бандла для /rpc/v1
Тяжело — обычно overkill.
Стратегия C: только info.version
Штампуйте бандл как v2.4.0, deprecate’те методы по отдельности:
#[Rpc\Method('user.legacy_get', deprecated: 'Use user.get instead.')]
Рекомендуемый дефолт.
Ограничения
Schema builder покрывает PHP-типы + curated набор Validator-констрейнтов. Что не моделируется:
- List-like массивы —
list<YourDto>,Foo[],array<int, YourDto>→{type: 'array', items: …}; для object-элементов — полная вложенная схема, для scalar-списков — пока только{type: 'array'}. - String-keyed map —
array<string, YourDto>(и scalar-значения) →{type: 'object', additionalProperties: …}в соответствии с JSON object map в JSONparamsклиента. Union в значении →additionalProperties.oneOf. - Нужен PHPDoc на параметре ctor или promoted-свойстве (
@var/@param);phpstan/phpdoc-parser+phpdocumentor/type-resolver— зависимости бандла. Без PHPDoc голыйarrayостаётся{type: 'array'}. - Кастомные Validator-констрейнты — мапятся только стандартные из
Symfony\Component\Validator\Constraints\*. Кастомные проходят валидацию, но в schema не отображаются. - Наследование / интерфейсы в return types — эмитятся как
{type: 'object'}.
Для полного контроля над schema (например, добавить кастомные examples или
description к отдельным свойствам) — генерируйте документ, постпроцессите
jq / скриптом, и коммитьте отредактированную версию.