Всем привет, сегодня поговорим о том, как подружить Symfony 4, JSON RPC и OpenAPI 3.
Данная статья рассчитана не на новичков, вы уже должны понимать как работать с Symfony, Depedency Injection и другими «страшными» вещами.
Сегодня рассмотрим одну конкретную реализацию JSON RPC.
Реализации
Есть множество реализаций JSON RPC для Symfony, в частности:
- wasinger/jsonrpc-bundle
- timiki/rpc-server-bundle
- NeoFusion/JsonRpcBundle
- insidestyles/json-rpc-bundle
- symfony-jsonrpc-http-server
О последней как раз и поговорим в данной статье. Данная библиотека несколько преимуществ, которые определили мой выбор.
Она разработана без привязки к какому либо фреймворку (yoanm/php-jsonrpc-server-sdk), есть бандл для Symfony, имеет несколько дополнительных пакетов, позволяющие добавить проверку входящих данных, автоматическую документацию, события и интерфейсы для возможности дополнить работу без переопределения.
Установка
Для начала устанавливаем symfony/skeleton.
$ composer create-project symfony/skeleton jsonrpc
Переходим в папку проекта.
$ cd jsonrpc
И устанавливаем необходимую библиотеку.
$ composer require yoanm/symfony-jsonrpc-http-server
Настраиваем.
// config/bundles.php
return [
...
SymfonyBundleFrameworkBundleFrameworkBundle::class => ['all' => true],
YoanmSymfonyJsonRpcHttpServerJsonRpcHttpServerBundle::class => ['all' => true],
...
];
# config/routes.yaml
json-rpc-endpoint:
resource: '@JsonRpcHttpServerBundle/Resources/config/routing/endpoint.xml'
# config/packages/json_rpc.yaml
json_rpc_http_server: ~
Добавляем сервис, который будет хранить все наши методы.
// src/MappingCollector.php
<?php
namespace App;
use YoanmJsonRpcServerDomainJsonRpcMethodAwareInterface;
use YoanmJsonRpcServerDomainJsonRpcMethodInterface;
class MappingCollector implements JsonRpcMethodAwareInterface
{
/** @var JsonRpcMethodInterface[] */
private $mappingList = [];
public function addJsonRpcMethod(string $methodName, JsonRpcMethodInterface $method): void
{
$this->mappingList[$methodName] = $method;
}
/**
* @return JsonRpcMethodInterface[]
*/
public function getMappingList() : array
{
return $this->mappingList;
}
}
И добавляем сервис в services.yaml.
# config/services.yaml
services:
...
mapping_aware_service:
class: AppMappingCollector
tags: ['json_rpc_http_server.method_aware']
...
Реализация методов
Методы JSON RPC добавляются как обычные сервисы в файле services.yaml. Реализуем сначала сам метод ping.
// src/Method/PingMethod.php
<?php
namespace AppMethod;
use YoanmJsonRpcServerDomainJsonRpcMethodInterface;
class PingMethod implements JsonRpcMethodInterface
{
public function apply(array $paramList = null)
{
return 'pong';
}
}
И добавим как сервис.
# config/services.yaml
services:
...
AppMethodPingMethod:
public: false
tags: [{ method: 'ping', name: 'json_rpc_http_server.jsonrpc_method' }]
...
Запускаем встроенный веб сервер Symfony.
$ symfony serve
Пробуем сделать вызов.
$ curl 'http://127.0.0.1:8000/json-rpc' --data-binary '[{ "jsonrpc":"2.0","method":"ping","params":[],"id" : 1 }]'
[
{
"jsonrpc": "2.0",
"id": 1,
"result": "pong"
}
]
Теперь реализуем метод, получающий параметры. В качестве ответа вернем входные данные.
// src/Method/ParamsMethod.php
<?php
namespace AppMethod;
use YoanmJsonRpcServerDomainJsonRpcMethodInterface;
class ParamsMethod implements JsonRpcMethodInterface
{
public function apply(array $paramList = null)
{
return $paramList;
}
}
# config/services.yaml
services:
...
AppMethodParamsMethod:
public: false
tags: [{ method: 'params', name: 'json_rpc_http_server.jsonrpc_method' }]
...
Пробуем вызвать.
$ curl 'http://127.0.0.1:8000/json-rpc' --data-binary '[{ "jsonrpc":"2.0","method":"params","params":{"name":"John","age":21},"id" : 1 }]'
[
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"name": "John",
"age": 21
}
}
]
Валидация входных данных метода
Если требуется автоматическая проверка данных на входе метода, то на этот случай есть пакет yoanm/symfony-jsonrpc-params-validator.
$ composer require yoanm/symfony-jsonrpc-params-validator
Подключаем бандл.
// config/bundles.php
return [
...
YoanmJsonRpcParamsValidatorBundleJsonRpcParamsValidatorBundle::class => ['all' => true],
...
];
Методы, которые нуждаются в проверке входных данных должны реализовать интерфейс YoanmJsonRpcParamsSymfonyValidatorDomainMethodWithValidatedParamsInterface. Изменим немного класс ParamsMethod.
// src/Method/ParamsMethod.php
<?php
namespace AppMethod;
use SymfonyComponentValidatorConstraint;
use SymfonyComponentValidatorConstraintsChoice;
use SymfonyComponentValidatorConstraintsCollection;
use SymfonyComponentValidatorConstraintsLength;
use SymfonyComponentValidatorConstraintsNotBlank;
use SymfonyComponentValidatorConstraintsOptional;
use SymfonyComponentValidatorConstraintsPositive;
use SymfonyComponentValidatorConstraintsRequired;
use YoanmJsonRpcParamsSymfonyValidatorDomainMethodWithValidatedParamsInterface;
use YoanmJsonRpcServerDomainJsonRpcMethodInterface;
class ParamsMethod implements JsonRpcMethodInterface, MethodWithValidatedParamsInterface
{
public function apply(array $paramList = null)
return $paramList;
}
public function getParamsConstraint() : Constraint
return new Collection(['fields' => [
'name' => new Required([
new Length(['min' => 1, 'max' => 32])
]),
'age' => new Required([
new Positive()
]),
'sex' => new Optional([
new Choice(['f', 'm'])
]),
]]);
}
}
Теперь если выполним запрос с пустыми параметрами или с ошибками, то получим в ответ соответствующие ошибки.
$ curl 'http://127.0.0.1:8000/json-rpc' --data-binary '[{"jsonrpc":"2.0","method":"params","params":[],"id" : 1 }]'
[
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Invalid params",
"data": {
"violations": [
{
"path": "[name]",
"message": "This field is missing.",
"code": "2fa2158c-2a7f-484b-98aa-975522539ff8"
},
{
"path": "[age]",
"message": "This field is missing.",
"code": "2fa2158c-2a7f-484b-98aa-975522539ff8"
}
]
}
}
}
]
$ curl 'http://127.0.0.1:8000/json-rpc' --data-binary '[{"jsonrpc":"2.0","method":"params","params":{"name":"John","age":-1},"id" : 1 }]'
[
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Invalid params",
"data": {
"violations": [
{
"path": "[age]",
"message": "This value should be positive.",
"code": "778b7ae0-84d3-481a-9dec-35fdb64b1d78"
}
]
}
}
}
$ curl 'http://127.0.0.1:8000/json-rpc' --data-binary '[{ "jsonrpc":"2.0","method":"params","params":{"name":"John","age":21,"sex":"u"},"id" : 1 }]'
[
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Invalid params",
"data": {
"violations": [
{
"path": "[sex]",
"message": "The value you selected is not a valid choice.",
"code": "8e179f1b-97aa-4560-a02f-2a8b42e49df7"
}
]
}
}
}
]
Автодокументация
Устанавливаем дополнительный пакет.
composer require yoanm/symfony-jsonrpc-http-server-doc
Настраиваем бандл.
// config/bundles.php
return [
...
YoanmSymfonyJsonRpcHttpServerDocJsonRpcHttpServerDocBundle::class => ['all' => true],
...
];
# config/routes.yaml
...
json-rpc-endpoint-doc:
resource: '@JsonRpcHttpServerDocBundle/Resources/config/routing/endpoint.xml'
# config/packages/json_rpc.yaml
...
json_rpc_http_server_doc: ~
Теперь можно получить документацию в JSON формате.
$ curl 'http://127.0.0.1:8000/doc'
{
"methods": [
{
"identifier": "Params",
"name": "params"
},
{
"identifier": "Ping",
"name": "ping"
}
],
"errors": [
{
"id": "ParseError-32700",
"title": "Parse error",
"type": "object",
"properties": {
"code": -32700
}
},
{
"id": "InvalidRequest-32600",
"title": "Invalid request",
"type": "object",
"properties": {
"code": -32600
}
},
{
"id": "MethodNotFound-32601",
"title": "Method not found",
"type": "object",
"properties": {
"code": -32601
}
},
{
"id": "ParamsValidationsError-32602",
"title": "Params validations error",
"type": "object",
"properties": {
"code": -32602,
"data": {
"type": "object",
"nullable": true,
"required": true,
"siblings": {
"violations": {
"type": "array",
"nullable": true,
"required": false
}
}
}
}
},
{
"id": "InternalError-32603",
"title": "Internal error",
"type": "object",
"properties": {
"code": -32603,
"data": {
"type": "object",
"nullable": true,
"required": false,
"siblings": {
"previous": {
"description": "Previous error message",
"type": "string",
"nullable": true,
"required": false
}
}
}
}
}
],
"http": {
"host": "127.0.0.1:8000"
}
}
Но как же так? А где описание входных параметров? Для этого нужно поставить еще один бандл yoanm/symfony-jsonrpc-params-sf-constraints-doc.
$ composer require yoanm/symfony-jsonrpc-params-sf-constraints-doc
// config/bundles.php
return [
...
YoanmSymfonyJsonRpcParamsSfConstraintsDocJsonRpcParamsSfConstraintsDocBundle::class => ['all' => true],
...
];
Теперь если сделать запрос, то получим JSON уже методы с параметрами.
$ curl 'http://127.0.0.1:8000/doc'
{
"methods": [
{
"identifier": "Params",
"name": "params",
"params": {
"type": "object",
"nullable": false,
"required": true,
"siblings": {
"name": {
"type": "string",
"nullable": true,
"required": true,
"minLength": 1,
"maxLength": 32
},
"age": {
"type": "string",
"nullable": true,
"required": true
},
"sex": {
"type": "string",
"nullable": true,
"required": false,
"allowedValues": [
"f",
"m"
]
}
}
}
},
{
"identifier": "Ping",
"name": "ping"
}
],
"errors": [
{
"id": "ParseError-32700",
"title": "Parse error",
"type": "object",
"properties": {
"code": -32700
}
},
{
"id": "InvalidRequest-32600",
"title": "Invalid request",
"type": "object",
"properties": {
"code": -32600
}
},
{
"id": "MethodNotFound-32601",
"title": "Method not found",
"type": "object",
"properties": {
"code": -32601
}
},
{
"id": "ParamsValidationsError-32602",
"title": "Params validations error",
"type": "object",
"properties": {
"code": -32602,
"data": {
"type": "object",
"nullable": true,
"required": true,
"siblings": {
"violations": {
"type": "array",
"nullable": true,
"required": false,
"item_validation": {
"type": "object",
"nullable": true,
"required": true,
"siblings": {
"path": {
"type": "string",
"nullable": true,
"required": true,
"example": "[key]"
},
"message": {
"type": "string",
"nullable": true,
"required": true
},
"code": {
"type": "string",
"nullable": true,
"required": false
}
}
}
}
}
}
}
},
{
"id": "InternalError-32603",
"title": "Internal error",
"type": "object",
"properties": {
"code": -32603,
"data": {
"type": "object",
"nullable": true,
"required": false,
"siblings": {
"previous": {
"description": "Previous error message",
"type": "string",
"nullable": true,
"required": false
}
}
}
}
}
],
"http": {
"host": "127.0.0.1:8000"
}
}
OpenAPI 3
Для того, чтобы JSON документация была совместима со стандартом OpenAPI 3, нужно установить yoanm/symfony-jsonrpc-http-server-openapi-doc.
$ composer require yoanm/symfony-jsonrpc-http-server-openapi-doc
Настраиваем.
// config/bundles.php
return [
...
YoanmSymfonyJsonRpcHttpServerOpenAPIDocJsonRpcHttpServerOpenAPIDocBundle::class => ['all' => true],
...
];
Сделав новый запрос, мы получим JSON документацию в формате OpenApi 3.
$ curl 'http://127.0.0.1:8000/doc/openapi.json'
{
"openapi": "3.0.0",
"servers": [
{
"url": "http://127.0.0.1:8000"
}
],
"paths": {
"/Params/../json-rpc": {
"post": {
"summary": ""params" json-rpc method",
"operationId": "Params",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"allOf": [
{
"type": "object",
"required": [
"jsonrpc",
"method"
],
"properties": {
"id": {
"example": "req_id",
"oneOf": [
{
"type": "string"
},
{
"type": "number"
}
]
},
"jsonrpc": {
"type": "string",
"example": "2.0"
},
"method": {
"type": "string"
},
"params": {
"title": "Method parameters"
}
}
},
{
"type": "object",
"required": [
"params"
],
"properties": {
"params": {
"$ref": "#/components/schemas/Method-Params-RequestParams"
}
}
},
{
"type": "object",
"properties": {
"method": {
"example": "params"
}
}
}
]
}
}
}
},
"responses": {
"200": {
"description": "JSON-RPC response",
"content": {
"application/json": {
"schema": {
"allOf": [
{
"type": "object",
"required": [
"jsonrpc"
],
"properties": {
"id": {
"example": "req_id",
"oneOf": [
{
"type": "string"
},
{
"type": "number"
}
]
},
"jsonrpc": {
"type": "string",
"example": "2.0"
},
"result": {
"title": "Result"
},
"error": {
"title": "Error"
}
}
},
{
"type": "object",
"properties": {
"result": {
"description": "Method result"
}
}
},
{
"type": "object",
"properties": {
"error": {
"oneOf": [
{
"$ref": "#/components/schemas/ServerError-ParseError-32700"
},
{
"$ref": "#/components/schemas/ServerError-InvalidRequest-32600"
},
{
"$ref": "#/components/schemas/ServerError-MethodNotFound-32601"
},
{
"$ref": "#/components/schemas/ServerError-ParamsValidationsError-32602"
},
{
"$ref": "#/components/schemas/ServerError-InternalError-32603"
}
]
}
}
}
]
}
}
}
}
}
}
},
"/Ping/../json-rpc": {
"post": {
"summary": ""ping" json-rpc method",
"operationId": "Ping",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"allOf": [
{
"type": "object",
"required": [
"jsonrpc",
"method"
],
"properties": {
"id": {
"example": "req_id",
"oneOf": [
{
"type": "string"
},
{
"type": "number"
}
]
},
"jsonrpc": {
"type": "string",
"example": "2.0"
},
"method": {
"type": "string"
},
"params": {
"title": "Method parameters"
}
}
},
{
"type": "object",
"properties": {
"method": {
"example": "ping"
}
}
}
]
}
}
}
},
"responses": {
"200": {
"description": "JSON-RPC response",
"content": {
"application/json": {
"schema": {
"allOf": [
{
"type": "object",
"required": [
"jsonrpc"
],
"properties": {
"id": {
"example": "req_id",
"oneOf": [
{
"type": "string"
},
{
"type": "number"
}
]
},
"jsonrpc": {
"type": "string",
"example": "2.0"
},
"result": {
"title": "Result"
},
"error": {
"title": "Error"
}
}
},
{
"type": "object",
"properties": {
"result": {
"description": "Method result"
}
}
},
{
"type": "object",
"properties": {
"error": {
"oneOf": [
{
"$ref": "#/components/schemas/ServerError-ParseError-32700"
},
{
"$ref": "#/components/schemas/ServerError-InvalidRequest-32600"
},
{
"$ref": "#/components/schemas/ServerError-MethodNotFound-32601"
},
{
"$ref": "#/components/schemas/ServerError-ParamsValidationsError-32602"
},
{
"$ref": "#/components/schemas/ServerError-InternalError-32603"
}
]
}
}
}
]
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Method-Params-RequestParams": {
"type": "object",
"nullable": false,
"required": [
"name",
"age"
],
"properties": {
"name": {
"type": "string",
"nullable": true,
"minLength": 1,
"maxLength": 32
},
"age": {
"type": "string",
"nullable": true
},
"sex": {
"type": "string",
"nullable": true,
"enum": [
"f",
"m"
]
}
}
},
"ServerError-ParseError-32700": {
"title": "Parse error",
"allOf": [
{
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "number"
},
"message": {
"type": "string"
}
}
},
{
"type": "object",
"required": [
"code"
],
"properties": {
"code": {
"example": -32700
}
}
}
]
},
"ServerError-InvalidRequest-32600": {
"title": "Invalid request",
"allOf": [
{
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "number"
},
"message": {
"type": "string"
}
}
},
{
"type": "object",
"required": [
"code"
],
"properties": {
"code": {
"example": -32600
}
}
}
]
},
"ServerError-MethodNotFound-32601": {
"title": "Method not found",
"allOf": [
{
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "number"
},
"message": {
"type": "string"
}
}
},
{
"type": "object",
"required": [
"code"
],
"properties": {
"code": {
"example": -32601
}
}
}
]
},
"ServerError-ParamsValidationsError-32602": {
"title": "Params validations error",
"allOf": [
{
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "number"
},
"message": {
"type": "string"
}
}
},
{
"type": "object",
"required": [
"code",
"data"
],
"properties": {
"code": {
"example": -32602
},
"data": {
"type": "object",
"nullable": true,
"properties": {
"violations": {
"type": "array",
"nullable": true,
"items": {
"type": "object",
"nullable": true,
"required": [
"path",
"message"
],
"properties": {
"path": {
"type": "string",
"nullable": true,
"example": "[key]"
},
"message": {
"type": "string",
"nullable": true
},
"code": {
"type": "string",
"nullable": true
}
}
}
}
}
}
}
}
]
},
"ServerError-InternalError-32603": {
"title": "Internal error",
"allOf": [
{
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "number"
},
"message": {
"type": "string"
}
}
},
{
"type": "object",
"required": [
"code"
],
"properties": {
"code": {
"example": -32603
},
"data": {
"type": "object",
"nullable": true,
"properties": {
"previous": {
"description": "Previous error message",
"type": "string",
"nullable": true
}
}
}
}
}
]
}
}
}
}
Документация ответа метода
Штатного функционала (например путем реализации интерфейса), позволяющего добавлять ответы методов в документацию, нет. Но есть возможность, путем подписки на события, добавить нужную информацию самостоятельно.
Добавляем слушателя.
# config/services.yaml
services:
...
AppListenerMethodDocListener:
tags:
- name: 'kernel.event_listener'
event: 'json_rpc_http_server_doc.method_doc_created'
method: 'enhanceMethodDoc'
- name: 'kernel.event_listener'
event: 'json_rpc_http_server_openapi_doc.array_created'
method: 'enhanceDoc'
...
// src/Listener/MethodDocListener.php
<?php
namespace AppListener;
use AppDomainJsonRpcMethodWithDocInterface;
use YoanmJsonRpcServerDocDomainModelErrorDoc;
use YoanmSymfonyJsonRpcHttpServerDocEventMethodDocCreatedEvent;
use YoanmSymfonyJsonRpcHttpServerOpenAPIDocEventOpenAPIDocCreatedEvent;
class MethodDocListener
{
public function enhanceMethodDoc(MethodDocCreatedEvent $event) : void
{
$method = $event->getMethod();
if ($method instanceof JsonRpcMethodWithDocInterface) {
$doc = $event->getDoc();
$doc->setResultDoc($method->getDocResponse());
foreach ($method->getDocErrors() as $error) {
if ($error instanceof ErrorDoc) {
$doc->addCustomError($error);
}
}
$doc->setDescription($method->getDocDescription());
$doc->addTag($method->getDocTag());
}
}
public function enhanceDoc(OpenAPIDocCreatedEvent $event)
{
$doc = $event->getOpenAPIDoc();
$doc['info'] = [
'title' => 'Main title',
'version' => '1.0.0',
'description' => 'Main description'
];
$event->setOpenAPIDoc($doc);
}
}
Еще, для того, чтобы прямо в слушателе не описывать документацию методов, сделаем интерфейс, который должны будут реализовывать сами методы.
// src/Domain/JsonRpcMethodWithDocInterface.php
<?php
namespace AppDomain;
use YoanmJsonRpcServerDocDomainModelErrorDoc;
use YoanmJsonRpcServerDocDomainModelTypeTypeDoc;
interface JsonRpcMethodWithDocInterface
{
/**
* @return TypeDoc
*/
public function getDocResponse(): TypeDoc;
/**
* @return ErrorDoc[]
*/
public function getDocErrors(): array;
/**
* @return string
*/
public function getDocDescription(): string;
/**
* @return string
*/
public function getDocTag(): string;
}
Теперь добавим новый метод, который будет в себе содержать нужную информацию.
// src/Method/UserMethod.php
<?php
namespace AppMethod;
use AppDomainJsonRpcMethodWithDocInterface;
use SymfonyComponentValidatorConstraint;
use SymfonyComponentValidatorConstraintsChoice;
use SymfonyComponentValidatorConstraintsCollection;
use SymfonyComponentValidatorConstraintsLength;
use SymfonyComponentValidatorConstraintsNotBlank;
use SymfonyComponentValidatorConstraintsOptional;
use SymfonyComponentValidatorConstraintsPositive;
use SymfonyComponentValidatorConstraintsRequired;
use YoanmJsonRpcParamsSymfonyValidatorDomainMethodWithValidatedParamsInterface;
use YoanmJsonRpcServerDomainJsonRpcMethodInterface;
use YoanmJsonRpcServerDocDomainModelErrorDoc;
use YoanmJsonRpcServerDocDomainModelTypeArrayDoc;
use YoanmJsonRpcServerDocDomainModelTypeNumberDoc;
use YoanmJsonRpcServerDocDomainModelTypeObjectDoc;
use YoanmJsonRpcServerDocDomainModelTypeStringDoc;
use YoanmJsonRpcServerDocDomainModelTypeTypeDoc;
class UserMethod implements JsonRpcMethodInterface, MethodWithValidatedParamsInterface, JsonRpcMethodWithDocInterface
{
public function apply(array $paramList = null)
{
return [
'name' => $paramList['name'],
'age' => $paramList['age'],
'sex' => $paramList['sex'] ?? null,
];
}
public function getParamsConstraint() : Constraint
{
return new Collection(['fields' => [
'name' => new Required([
new Length(['min' => 1, 'max' => 32])
]),
'age' => new Required([
new Positive()
]),
'sex' => new Optional([
new Choice(['f', 'm'])
]),
]]);
}
public function getDocDescription(): string
{
return 'User method';
}
public function getDocTag(): string
{
return 'main';
}
public function getDocErrors(): array
{
return [new ErrorDoc('Error 1', 1)];
}
public function getDocResponse(): TypeDoc
{
$response = new ObjectDoc();
$response->setNullable(false);
$response->addSibling((new StringDoc())
->setNullable(false)
->setDescription('Name of user')
->setName('name')
);
$response->addSibling((new NumberDoc())
->setNullable(false)
->setDescription('Age of user')
->setName('age')
);
$response->addSibling((new StringDoc())
->setNullable(true)
->setDescription('Sex of user')
->setName('sex')
);
return $response;
}
}
Не забываем прописать новый сервис.
services:
...
AppMethodUserMethod:
public: false
tags: [{ method: 'user', name: 'json_rpc_http_server.jsonrpc_method' }]
...
Теперь сделав новый запрос к /doc/openapi.json, получим новые данные.
curl 'http://127.0.0.1:8000/doc/openapi.json'
{
"openapi": "3.0.0",
"servers": [
{
"url": "http://127.0.0.1:8000"
}
],
"paths": {
...
"/User/../json-rpc": {
"post": {
"summary": ""user" json-rpc method",
"description": "User method",
"tags": [
"main"
],
...
"responses": {
"200": {
"description": "JSON-RPC response",
"content": {
"application/json": {
"schema": {
"allOf": [
...
{
"type": "object",
"properties": {
"result": {
"$ref": "#/components/schemas/Method-User-Result"
}
}
},
{
"type": "object",
"properties": {
"error": {
"oneOf": [
{
"$ref": "#/components/schemas/Error-Error11"
},
...
]
}
}
}
]
}
}
}
}
}
}
}
},
"components": {
"schemas": {
...
"Method-User-Result": {
"type": "object",
"nullable": false,
"properties": {
"name": {
"description": "Name of user",
"type": "string",
"nullable": false
},
"age": {
"description": "Age of user",
"type": "number",
"nullable": false
},
"sex": {
"description": "Sex of user",
"type": "string",
"nullable": true
}
}
},
"Error-Error11": {
"title": "Error 1",
"allOf": [
{
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "number"
},
"message": {
"type": "string"
}
}
},
{
"type": "object",
"required": [
"code"
],
"properties": {
"code": {
"example": 1
}
}
}
]
},
...
}
},
"info": {
"title": "Main title",
"version": "1.0.0",
"description": "Main description"
}
}
Визуализация JSON документации
JSON это круто, но люди обычно хотят видеть более человечный результат. Файл /doc/openapi.json можно отдать внешним сервисам визуализации, например Swagger Editor.
При желании можно установить Swagger UI и в нашем проекте. Воспользуемся пакетом harmbandstra/swagger-ui-bundle.
Для корректной публикации ресурсов добавляем с composer.json следующее.
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"HarmBandstra\SwaggerUiBundle\Composer\ScriptHandler::linkAssets",
"@auto-scripts"
],
"post-update-cmd": [
"HarmBandstra\SwaggerUiBundle\Composer\ScriptHandler::linkAssets",
"@auto-scripts"
]
},
После ставим пакет.
$ composer require harmbandstra/swagger-ui-bundle
Подключаем бандл.
// config/bundles.php
<?php
return [
// ...
HarmBandstraSwaggerUiBundleHBSwaggerUiBundle::class => ['dev' => true]
];
# config/routes.yaml
_swagger-ui:
resource: '@HBSwaggerUiBundle/Resources/config/routing.yml'
prefix: /docs
# config/packages/hb_swagger_ui.yaml
hb_swagger_ui:
directory: "http://127.0.0.1:8000"
files:
- "/doc/openapi.json"
Теперь перейдя по ссылке http://127.0.0.1:8000/docs/ получим документацию в красивом виде.
Итоги
В результате все проведенных манипуляций мы получили работающий JSON RPC на базе Symfony 4 и автоматическую документацию OpenAPI с визуализацией с помощью Swagger UI.
Всем спасибо.
Автор: vladdnepr