Коротко: 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-api

Flex-рецепт обычно всё подключает сам. Если ставите вручную — проверьте, что бандл зарегистрирован в 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 — имя метода, которое клиент указывает в поле method JSON-RPC-запроса;
  • type — HTTP-метод (обычно POST);
  • roles — список ролей для доступа. Пустой массив (или отсутствие) = метод публичный; непустой — доступ проверяется через isGranted(), и при отказе клиент получает 403 Access 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 с идеями всегда в тему.