otezvikentiy / json-rpc-api
Symfony Json RPC API bundle
Package info
github.com/OtezVikentiy/symfony-jsonrpc-api-bundle
Type:symfony-bundle
pkg:composer/otezvikentiy/json-rpc-api
Requires
- php: >=8.2
- doctrine/annotations: >=2.0
- symfony/console: >=6.4
- symfony/framework-bundle: >=6.4
- symfony/security-bundle: >=6.4
- symfony/serializer: >=6.4
- symfony/validator: >=6.4
- symfony/yaml: >=6.4
Requires (Dev)
- phpunit/phpunit: 11.*
- symfony/property-access: >=6.4
- dev-master
- 4.1
- 4.0
- 3.9
- 3.8
- 3.7
- 3.6
- 3.5
- 3.4
- 3.3
- 3.2
- 2.18
- 2.17
- 2.16
- 2.14
- 2.12
- 2.11
- 2.10
- 2.8
- 2.7
- 2.6
- 2.5
- 2.4
- 2.3
- 2.2
- 2.1
- 2.0
- 1.36
- 1.34
- 1.33
- 1.32
- 1.31
- 1.30
- 1.22
- 1.20
- 1.19
- 1.18
- 1.17
- 1.16
- 1.15
- 1.14
- 1.13
- 1.12
- 1.11
- 1.10
- 1.9
- 1.8
- 1.7
- 1.3
- 1.1.7
- 1.1.6
- 1.0.21
- 1.0.20
- 1.0.18
- 1.0.17
- 1.0.16
- 1.0.15
- dev-patch_4.0
- dev-RZ
This package is auto-updated.
Last update: 2026-05-14 15:22:38 UTC
README
Symfony-бандл для быстрого и удобного создания JSON-RPC 2.0 API приложений.
GitHub: https://github.com/OtezVikentiy/symfony-jsonrpc-api-bundle
Возможности
- Полная совместимость со спецификацией JSON-RPC 2.0
- Конфигурация методов через PHP 8 атрибуты (
#[JsonRPCAPI(...)]) - Поддержка HTTP-методов: POST, GET, PUT, PATCH, DELETE
- Версионирование API (
/api/v1,/api/v2, ...) - Автоматическая генерация OpenAPI/Swagger документации
- Pre- и Post-процессоры (middleware)
- Пакетные запросы (batch requests)
- Встроенная валидация запросов
- Ролевой контроль доступа через Symfony Security
- Поддержка бинарных ответов (изображения, документы)
Требования
- PHP >= 8.2
- Symfony >= 6.4
Установка
composer require otezvikentiy/json-rpc-api
Включите бандл (если не используется Symfony Flex):
// config/bundles.php return [ // ... OV\JsonRPCAPIBundle\OVJsonRPCAPIBundle::class => ['all' => true], ];
Создайте конфигурационные файлы:
# config/routes/ov_json_rpc_api.yaml ov_json_rpc_api: resource: '@OVJsonRPCAPIBundle/config/routes/routes.yaml'
# config/packages/ov_json_rpc_api.yaml ov_json_rpc_api: access_control_allow_origin_list: - '*' swagger: api_v1: api_version: '1' base_path: '%env(string:OV_JSON_RPC_API_BASE_URL)%' base_path_description: 'Production server' test_path: '%env(string:OV_JSON_RPC_API_TEST_URL)%' test_path_description: 'Sandbox server' auth_token_name: 'X-AUTH-TOKEN' auth_token_test_value: '%env(string:OV_JSON_RPC_API_AUTH_TOKEN)%' info: title: 'My API' description: 'JSON-RPC 2.0 API' terms_of_service_url: 'https://example.com/tos' contact: name: 'Support' url: 'https://example.com' email: 'support@example.com' license: 'MIT' licenseUrl: 'https://opensource.org/licenses/MIT'
# .env OV_JSON_RPC_API_SWAGGER_PATH=public/openapi/ OV_JSON_RPC_API_BASE_URL=http://localhost OV_JSON_RPC_API_TEST_URL=http://localhost OV_JSON_RPC_API_AUTH_TOKEN=your_test_token_here
Подробная инструкция: docs/installation.md
Быстрый старт
1. Создайте Request
// src/RPC/V1/GetProduct/Request.php namespace App\RPC\V1\GetProduct; class Request { private int $id; private string $title; public function __construct(int $id) { $this->id = $id; } public function getId(): int { return $this->id; } public function setId(int $id): void { $this->id = $id; } public function getTitle(): string { return $this->title; } public function setTitle(string $title): void { $this->title = $title; } }
2. Создайте Response
// src/RPC/V1/GetProduct/Response.php namespace App\RPC\V1\GetProduct; class Response { private bool $success; private string $title; private int $price; public function __construct(bool $success = true) { $this->success = $success; } public function isSuccess(): bool { return $this->success; } public function setSuccess(bool $success): void { $this->success = $success; } public function getTitle(): string { return $this->title; } public function setTitle(string $title): void { $this->title = $title; } public function getPrice(): int { return $this->price; } public function setPrice(int $price): void { $this->price = $price; } }
3. Создайте метод API
// src/RPC/V1/GetProductMethod.php namespace App\RPC\V1; use OV\JsonRPCAPIBundle\Core\Annotation\JsonRPCAPI; use OV\JsonRPCAPIBundle\Core\ApiMethodInterface; use App\RPC\V1\GetProduct\Request; use App\RPC\V1\GetProduct\Response; #[JsonRPCAPI(methodName: 'getProduct', type: 'POST')] class GetProductMethod implements ApiMethodInterface { public function call(Request $request): Response { $response = new Response(); $response->setTitle('Iphone 15'); $response->setPrice(2000); return $response; } }
4. Вызовите API
curl -X POST http://localhost/api/v1 \ -H "Content-Type: application/json" \ -d '{"jsonrpc": "2.0", "method": "getProduct", "params": {"id": 1, "title": "test"}, "id": "1"}'
Ответ:
{
"jsonrpc": "2.0",
"result": {
"success": true,
"title": "Iphone 15",
"price": 2000
},
"id": "1"
}
Архитектура
Пайплайн обработки запроса
HTTP POST /api/v{version}
|
v
ApiController
|
v
RequestRawDataHandler --- парсит HTTP-запрос (JSON body / query params)
|
v
BatchStrategyFactory --- определяет: одиночный или пакетный запрос
|
v
RequestHandler
|--- Поиск MethodSpec по имени метода
|--- Создание объекта Request из параметров
|--- Валидация типизированных свойств
|--- PreProcessors (если есть)
|--- Method::call(Request) -> Response
|--- PostProcessors (если есть)
|
v
ResponseService --- сериализация ответа в JSON-RPC 2.0 формат
Структура проекта API-метода
src/RPC/V1/
GetProductMethod.php # Класс метода с #[JsonRPCAPI] атрибутом
GetProduct/
Request.php # DTO входящего запроса
Response.php # DTO ответа
Классы, реализующие ApiMethodInterface и помеченные атрибутом #[JsonRPCAPI], автоматически обнаруживаются и регистрируются бандлом.
Примеры
| Пример | Описание | Файлы |
|---|---|---|
| Базовый | Простейший пример создания API-метода | Request, Response, Method |
| Pre/Post-процессоры | Выполнение логики до и после вызова метода | Request, Response, Method, AbstractMethod |
| Массив объектов | Возврат коллекции объектов в ответе | Request, Response, Method, Product |
| Бинарный ответ | Возврат изображений, документов и других бинарных данных | Request, PlainResponse, Method |
Дополнительная документация
| Раздел | Описание |
|---|---|
| Обработка ошибок | Коды ошибок, JRPCException, кастомные ошибки, additionalInfo |
| Notification-запросы | Запросы без id, параметр strict_notifications |
| Валидация параметров | Автоматическая валидация типов, nullable, формат ошибок |
| Базовый класс JsonRpcRequest | Метод toArray(), рекурсивная сериализация |
| Partial updates (JSON Merge Patch) | PartialRequestInterface, wasProvided(), RFC 7396 семантика |
| Troubleshooting / FAQ | Типичные проблемы и их решения |
| CHANGELOG | История изменений по версиям |
Logging
Опциональная подсистема Request/Response логирования с маскировкой sensitive-данных через PSR-3 logger. По умолчанию выключена. Подробности — docs/logging.md.
Partial updates (JSON Merge Patch)
Бандл поддерживает PATCH-семантику по RFC 7396 для Update-методов, где клиент шлёт только изменённые поля.
Проблема: при стандартном паттерне if ($request->getX() !== null) { $entity->setX($request->getX()); } нельзя различить «поле не передано» и «поле передано как null» — оба случая дают null в DTO. Это значит, что нельзя очистить поле через PATCH.
Решение: Request DTO реализует PartialRequestInterface, и фреймворк отслеживает, какие поля реально пришли в payload. Сервис-слой использует wasProvided('x') вместо !== null:
use OV\JsonRPCAPIBundle\Core\Request\PartialUpdateRequest; class UpdateUserRequest extends PartialUpdateRequest { private ?int $id = null; private ?string $email = null; private ?string $bio = null; // getters/setters... }
public function call(UpdateUserRequest $request): Response { $user = $this->userRepository->find($request->getId()); if ($request->wasProvided('email')) { $user->setEmail($request->getEmail()); // null = очистить } if ($request->wasProvided('bio')) { $user->setBio($request->getBio()); } // ... }
Семантика payload-а:
| Payload | wasProvided |
Поведение в сервисе |
|---|---|---|
{"email": "new@x.com"} |
true |
установить новое значение |
{"email": null} |
true |
очистить поле (null) |
{} (ключ отсутствует) |
false |
не трогать поле |
Опт-ин: только DTO, реализующие PartialRequestInterface, получают трекинг. Существующие DTO работают без изменений (полная обратная совместимость).
Подробности и edge-кейсы — в docs/partial_updates.md.
Версионирование API
Версия API определяется из URL (/api/v1, /api/v2) или явно через параметр version в атрибуте:
#[JsonRPCAPI(methodName: 'getProduct', type: 'POST', version: 2)]
Если version не указан, он извлекается из пространства имён класса (например, App\RPC\V1 -> версия 1).
Пакетные запросы (Batch)
Бандл поддерживает пакетные JSON-RPC запросы согласно спецификации:
curl -X POST http://localhost/api/v1 \ -H "Content-Type: application/json" \ -d '[ {"jsonrpc": "2.0", "method": "sum", "params": [1, 2, 4], "id": "1"}, {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}, {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": "2"} ]'
Pre- и Post-процессоры
Процессоры позволяют выполнять логику до и после вызова метода API (логирование, аудит, уведомления и т.д.):
use OV\JsonRPCAPIBundle\Core\PreProcessorInterface; use OV\JsonRPCAPIBundle\Core\PostProcessorInterface; #[JsonRPCAPI(methodName: 'getProduct', type: 'POST')] class GetProductMethod implements PreProcessorInterface, PostProcessorInterface { public function getPreProcessors(): array { return [ static::class => ['logRequest'], ]; } public function getPostProcessors(): array { return [ static::class => ['logResponse'], ]; } public function logRequest(string $processorClass, ?object $request = null): void { // Вызывается ПЕРЕД call() } public function logResponse(string $processorClass, ?object $request = null, ?OvResponseInterface $response = null): void { // Вызывается ПОСЛЕ call() } public function call(Request $request): Response { // Основная логика } }
Подробнее: docs/examples/pre-and-post-processors.md
Swagger / OpenAPI
Генерация документации
bin/console ov:swagger:generate
Генерирует файл public/openapi/api_v1.yaml, который можно использовать в Swagger UI.
Аннотации для документации
Скалярные свойства:
use OV\JsonRPCAPIBundle\Core\Annotation\SwaggerProperty; class Response { #[SwaggerProperty(default: 'true', example: 'true')] private bool $success; #[SwaggerProperty(format: 'email', example: 'user@example.com')] private string $email; }
Массивы:
use OV\JsonRPCAPIBundle\Core\Annotation\SwaggerArrayProperty; class Response { #[SwaggerArrayProperty(type: 'string')] private array $errors = []; #[SwaggerArrayProperty(type: Product::class, ofClass: true)] private array $products = []; }
Теги для группировки:
#[JsonRPCAPI(methodName: 'getProduct', type: 'POST', tags: ['products'])]
Подробнее:
Безопасность
Ролевой доступ
Ограничение доступа к методам по ролям через атрибут roles:
#[JsonRPCAPI(
methodName: 'deleteUser',
type: 'POST',
roles: ['ROLE_ADMIN', 'ROLE_SUPER_ADMIN']
)]
class DeleteUserMethod
{
public function call(Request $request): Response { /* ... */ }
}
При отсутствии нужной роли бандл возвращает HTTP 403.
Аутентификация
Бандл совместим с любым способом аутентификации Symfony:
Тестирование
Запуск тестов:
./vendor/bin/phpunit tests/
Покрытие требует драйвера (xdebug или pcov). В phpunit.xml.dist уже настроен <source>-блок для отчётов покрытия PHPUnit 10+.
Тестовый набор включает:
- Unit-тесты — все Core-компоненты, сервисы, модели запросов/ответов, DI, Swagger-модели.
- Интеграционные тесты — полный цикл обработки запросов через контроллер.
- Тесты команд — генерация Swagger YAML.
- Security regression-тесты (
tests/Security/) — DoS-лимиты (payload, batch, DTO depth, array size), sanitization ошибок, CORS origin matching, проверки видимости сеттеров, path-containment команды.
Гайд по написанию тестов для собственных RPC-методов — docs/testing.md.
Конфигурация
Параметры ov_json_rpc_api
| Параметр | По умолчанию | Описание |
|---|---|---|
access_control_allow_origin_list |
[] |
Разрешённые CORS-домены. ['*'] — wildcard; список конкретных origin'ов матчится с заголовком запроса Origin. |
cors_strict |
true |
При true — для origin'ов вне whitelist'а CORS-заголовок не отдаётся. При false — fallback к legacy comma-joined заголовку (невалидный, только для обратной совместимости). |
strict_notifications |
true |
Строгое следование JSON-RPC 2.0 для Notification-запросов (без id). При true — сервер не возвращает ответ (по спеку). При false — лояльный режим: ответ возвращается, если результат непустой (поведение 3.x). |
allow_extra_fields |
false |
При false — лишние поля в params, отсутствующие в Request DTO, вызывают INVALID_PARAMS. Можно переопределить per-method через #[JsonRPCAPI(allowExtraFields: true)]. |
expose_internal_errors |
false |
При false (production-safe) — uncaught non-JRPCException исключения возвращаются клиенту как Internal error., а оригинал пишется в LoggerInterface. При true — сырое сообщение отдаётся клиенту (для dev). |
max_payload_bytes |
1048576 |
Максимальный размер сырого тела запроса в байтах. Большие запросы — INVALID_REQUEST. |
max_json_depth |
64 |
Максимальная глубина вложенности JSON при декодировании. Глубже — PARSE_ERROR. |
max_batch_size |
50 |
Максимальное число запросов в одном JSON-RPC batch'е. Больше — единый INVALID_REQUEST. |
max_dto_depth |
10 |
Максимальная глубина рекурсии при гидратации вложенных Request DTO. Защита от stack/memory exhaustion. |
max_array_param_size |
1000 |
Максимальное число элементов массива-параметра, обрабатываемого через addX()-адеры. |
swagger |
— | Конфигурация Swagger по версиям API |
swagger.*.api_version |
'1' |
Номер версии API |
swagger.*.base_path |
— | URL production-сервера |
swagger.*.test_path |
null |
URL тестового сервера |
swagger.*.base_path_variables |
[] |
Переменные для подстановки в base_path |
swagger.*.test_path_variables |
[] |
Переменные для подстановки в test_path |
swagger.*.auth_token_name |
— | Имя заголовка для токена авторизации |
swagger.*.auth_token_test_value |
— | Тестовое значение токена |
swagger.*.info |
— | Информация об API (title, description, contact, license) |
Security hardening: рекомендации по значениям, обоснование и тюнинг для high-volume API — docs/security_hardening.md.
Параметры атрибута #[JsonRPCAPI]
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
methodName |
string | да | — | Имя JSON-RPC метода |
type |
string | да | — | HTTP-метод (POST, GET, PUT, PATCH, DELETE) |
version |
?int | нет | null |
Версия API (если null — определяется из namespace) |
summary |
string | нет | '' |
Краткое описание для Swagger |
description |
string | нет | '' |
Подробное описание для Swagger |
tags |
?array | нет | null |
Теги для группировки в Swagger |
roles |
array | нет | [] |
Требуемые роли для доступа |
ignoreInSwagger |
bool | нет | false |
Исключить метод из Swagger-документации |
group |
?string | нет | null |
Группа для пути в Swagger (например, 'products' → /products/get_product) |
Коды ошибок JSON-RPC
| Код | Константа | Описание |
|---|---|---|
-32700 |
PARSE_ERROR |
Ошибка парсинга JSON |
-32600 |
INVALID_REQUEST |
Невалидный JSON-RPC запрос |
-32601 |
METHOD_NOT_FOUND |
Метод не найден |
-32602 |
INVALID_PARAMS |
Невалидные параметры |
-32603 |
INTERNAL_ERROR |
Внутренняя ошибка |
-32000 |
SERVER_ERROR |
Серверная ошибка |
Лицензия
Автор
Leonid Groshev — OtezVikentiy@gmail.com — otezvikentiy.tech