Коротко: otezvikentiy/json-rpc-api — бандл для Symfony, который берёт на себя весь «обвес» JSON-RPC 2.0 API (роутинг, разбор запроса, валидацию, авторизацию по ролям, формирование ответа), а вам остаётся написать только сами методы. Один метод = класс с атрибутом #[JsonRPCAPI] плюс типизированные Request и Response. Ниже — как поднять API с нуля на актуальной версии 3.x.
Рано или поздно почти в любом сервисе появляется задача: отдавать данные наружу или принимать команды по API. И тут же встаёт «проблема белого листа»: с чего начать, какой стандарт выбрать, как не расписывать руками разбор запроса, валидацию и авторизацию для каждого метода. JSON-RPC 2.0 — простой и предсказуемый протокол: один endpoint, вызов метода по имени, параметры и ответ в JSON. А чтобы не писать инфраструктуру вокруг него самому, можно взять готовый бандл.
Ссылки: Packagist · GitHub. Дальше всё на версии 3.x (Symfony 6/7, PHP 8.1+).
Установка
Ставим бандла композером:
composer require otezvikentiy/json-rpc-apiFlex-рецепт обычно всё подключает сам. Если ставите вручную — проверьте, что бандл зарегистрирован в config/bundles.php:
<?php
// config/bundles.php
return [
// ...
OV\JsonRPCAPIBundle\OVJsonRPCAPIBundle::class => ['all' => true],
];И подключите роуты бандла в config/routes/ov_json_rpc_api.yaml:
# config/routes/ov_json_rpc_api.yaml
ov_json_rpc_api:
resource: '@OVJsonRPCAPIBundle/config/routes/routes.yaml'Важное отличие от старых версий: в 3.x методы регистрируются автоматически по атрибуту #[JsonRPCAPI]. Больше не нужно вручную прописывать в services.yaml тег ov.rpc.method и суффикс *Method.php — компилятор-пасс бандла сам находит ваши классы. На этом установка закончена.
Первый метод
Метод API в 3.x — это три класса в одной папке: сам метод, его Request и Response. Структура удобная — всё, что относится к методу, лежит рядом:
src/RPC/V1/
└── CreateTag/
├── Request.php
└── Response.php
└── CreateTag.phpМетод — final readonly class с атрибутом #[JsonRPCAPI] и единственным методом call(). Зависимости прокидываются через конструктор обычным autowiring'ом:
<?php
declare(strict_types=1);
namespace App\RPC\V1;
use App\Entity\ArticleTag;
use App\RPC\V1\CreateTag\Request;
use App\RPC\V1\CreateTag\Response;
use Doctrine\ORM\EntityManagerInterface;
use OV\JsonRPCAPIBundle\Core\Annotation\JsonRPCAPI;
#[JsonRPCAPI(methodName: 'CreateTag', type: 'POST', roles: ['ROLE_ADMIN'])]
final readonly class CreateTag
{
public function __construct(
private EntityManagerInterface $em,
) {
}
public function call(Request $request): Response
{
$name = trim($request->getName());
if ($name === '') {
return new Response(success: false, errors: ['name обязателен']);
}
$tag = new ArticleTag();
$tag->setName($name);
$this->em->persist($tag);
$this->em->flush();
return new Response(success: true, id: $tag->getId());
}
}Параметры атрибута:
methodName— имя метода, которое клиент указывает в полеmethodJSON-RPC-запроса;type— HTTP-метод (обычноPOST);roles— список ролей для доступа. Пустой массив (или отсутствие) = метод публичный; непустой — доступ проверяется черезisGranted(), и при отказе клиент получает 403Access not allowed;- ещё есть
version,group,allowExtraFields— о версиях ниже.
Request и Response
Request наследует JsonRpcRequest из бандла. Имена приватных свойств = имена JSON-параметров из params; бандл сам их распарсит и провалидирует по типам. Пишем типизированные геттеры/сеттеры:
<?php
declare(strict_types=1);
namespace App\RPC\V1\CreateTag;
use OV\JsonRPCAPIBundle\Core\Request\JsonRpcRequest;
class Request extends JsonRpcRequest
{
private string $name = '';
public function getName(): string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
}Response — обычный класс; его публичные свойства и попадают в поле result ответа. Удобно объявлять через промотированные свойства конструктора:
<?php
declare(strict_types=1);
namespace App\RPC\V1\CreateTag;
class Response
{
public function __construct(
public bool $success = true,
public ?int $id = null,
public array $errors = [],
) {
}
}Грабли, о которые легко споткнуться: для булева параметра геттер должен называться isX(), а не getX(). Поэтому само свойство именуйте без приставки is — например, свойство verified и геттер isVerified(). Иначе бандл не сопоставит параметр.
Вызов метода
Всё API живёт на одном endpoint /api/v1. Публичный метод дёргается обычным POST:
curl --header "Content-Type: application/json" \
--request POST \
--data '{"jsonrpc":"2.0","method":"CreateTag","params":{"name":"symfony"},"id":1}' \
https://example.com/api/v1В ответ приходит стандартный конверт JSON-RPC 2.0, где в result лежат публичные свойства вашего Response:
{"jsonrpc":"2.0","result":{"success":true,"id":42,"errors":[]},"id":1}Роли и авторизация
Ограничение доступа делается декларативно — прямо в атрибуте метода. Указали roles: ['ROLE_ADMIN'] — и бандл сам проверит права текущего пользователя. Вопрос лишь в том, как этот пользователь появляется в запросе к ^/api.
Файрвол для API — stateless (без сессий), а аутентификация вешается кастомными authenticator'ами. Типичная связка сегодня — токен по заголовку и/или JWT:
# config/packages/security.yaml
security:
firewalls:
api:
pattern: ^/api
stateless: true
provider: app_user_provider
custom_authenticators:
- App\Security\ApiKeyAuthenticator # X-AUTH-TOKEN
- App\Security\JwtAuthenticator # Bearer JWTОба authenticator'а стоит делать «опциональными»: их supports() возвращает true только когда во входящем запросе действительно есть соответствующий заголовок (X-AUTH-TOKEN или Authorization: Bearer). Тогда публичные методы работают без токена, а защищённые — требуют его. С токеном запрос выглядит так:
curl --header "X-AUTH-TOKEN: <ваш-токен>" \
--header "Content-Type: application/json" \
--request POST \
--data '{"jsonrpc":"2.0","method":"CreateTag","params":{"name":"symfony"},"id":1}' \
https://example.com/api/v1Сам механизм токена (сущность в БД, кастомный AbstractAuthenticator, JWT-сервис) реализуется стандартно по документации Symfony Security — бандл в это не вмешивается, он лишь читает уже аутентифицированного пользователя для проверки ролей.
Версии API
Версия задаётся неймспейсом папки (App\RPC\V1\..., App\RPC\V2\...) и, при необходимости, параметром version в атрибуте. Endpoint отражает версию — /api/v1, /api/v2 и т.д. Это позволяет держать несколько версий метода одновременно и мигрировать клиентов без ломающих изменений.
Итог
Чтобы поднять полноценное JSON-RPC 2.0 API на Symfony, достаточно: поставить бандл, подключить его роуты и на каждый метод создать три небольших класса. Роутинг, разбор запроса, валидация типов, проверка ролей и формат ответа — уже внутри. В версии 3.x стало ещё меньше рутины: методы регистрируются по атрибуту автоматически, а final readonly-классы с промотированными свойствами делают код компактным.
Код бандла и примеры — на GitHub. Если пользуетесь — звезда на репозитории и issue с идеями всегда в тему.