SOA (Сервис-Ориентированная Архитектура) строится путём комбинации и взаимодействия слабо-связанных сервисов.
Для демонстрации взаимодействия создадим два приложения Клиент и Сервер.
А их взаимодействие организуем посредством протокола удаленного вызова процедур JSON-RPC 2.0
.
Клиент
Приложение Клиент представляет собой сайт для создания и отображения некого контента. Клиент не содержит собственной базы данных, а получает и добавляет данные благодаря взаимодействию с приложением Сервер.
На клиенте взаимодействие обеспечивает класс JsonRpcClient
namespace ClientAppServices;
use GuzzleHttpClient;
use GuzzleHttpRequestOptions;
class JsonRpcClient
{
const JSON_RPC_VERSION = '2.0';
const METHOD_URI = 'data';
protected $client;
public function __construct()
{
$this->client = new Client([
'headers' => ['Content-Type' => 'application/json'],
'base_uri' => config('services.data.base_uri')
]);
}
public function send(string $method, array $params): array
{
$response = $this->client
->post(self::METHOD_URI, [
RequestOptions::JSON => [
'jsonrpc' => self::JSON_RPC_VERSION,
'id' => time(),
'method' => $method,
'params' => $params
]
])->getBody()->getContents();
return json_decode($response, true);
}
}
Нам потребуется библиотека GuzzleHttp
, предварительно устанавливаем ее.
Формируем вполне стандартный POST
запрос с помощью GuzzleHttpClient
. Основной нюанс здесь заключается в формате запроса.
Согласно спецификации JSON-RPC 2.0
запрос должен иметь вид:
{
"jsonrpc": "2.0",
"method": "getPageById",
"params": {
"page_uid": "f09f7c040131"
},
"id": "54645"
}
jsonrpc
версия протокола, должна быть указана «2.0»method
имя методаparams
массив с параметрамиid
идентификатор запроса
Ответ
{
"jsonrpc": "2.0",
"result": {
"id": 2,
"title": "Index Page",
"content": "Content",
"description": "Description",
"page_uid": "f09f7c040131"
},
"id": "54645"
}
Если запрос был выполнен с ошибкой, получаем
{
"jsonrpc": "2.0",
"error": {
"code": -32700,
"message": "Parse error"
},
"id": "null"
}
jsonrpc
версия протокола, должна быть указана «2.0»result
обязательное поле при успешном результате запроса. Не должно существовать при возникновении ошибкиerror
обязательное поле при возникновении ошибки. Не должно существовать при успешном результатеid
идентификатор запроса, установленный клиентом
Ответ формирует сервер, так что мы к нему еще вернемся.
В контроллере необходимо сформировать запрос с нужными параметрами и обработать ответ.
namespace ClientAppHttpControllers;
use AppServicesJsonRpcClient;
use IlluminateHttpRequest;
use IlluminateSupportFacadesRedirect;
class SiteController extends Controller
{
protected $client;
public function __construct(JsonRpcClient $client)
{
$this->client = $client;
}
public function show(Request $request)
{
$data = $this->client->send('getPageById', ['page_uid' => $request->get('page_uid')]);
if (empty($data['result'])) {
abort(404);
}
return view('page', ['data' => $data['result']]);
}
public function create()
{
return view('create-form');
}
public function store(Request $request)
{
$data = $this->client->send('create', $request->all());
if (isset($data['error'])) {
return Redirect::back()->withErrors($data['error']);
}
return view('page', ['data' => $data['result']]);
}
}
Фиксированный формат ответа JSON-RPC позволяет легко понять успешным ли был запрос и применить какие-либо действия, если ответ содержит ошибку.
Сервер
Начнем с настройки роутинга. В файл routes/api.php
добавим
Route::post('/data', function (Request $request, JsonRpcServer $server, DataController $controller) {
return $server->handle($request, $controller);
});
Все запросы поступившие на сервер по адресу <server_base_uri>/data
будут обработаны классом JsonRpcServer
namespace ServerAppServices;
class JsonRpcServer
{
public function handle(Request $request, Controller $controller)
{
try {
$content = json_decode($request->getContent(), true);
if (empty($content)) {
throw new JsonRpcException('Parse error', JsonRpcException::PARSE_ERROR);
}
$result = $controller->{$content['method']}(...[$content['params']]);
return JsonRpcResponse::success($result, $content['id']);
} catch (Exception $e) {
return JsonRpcResponse::error($e->getMessage());
}
}
}
Класс JsonRpcServer
связывает нужный метод контроллера с переданными параметрами. И возвращает ответ сформированный классом JsonRpcResponse
в формате согласно спецификации JSON-RPC 2.0
описанной выше.
use ServerAppHttpResponse;
class JsonRpcResponse
{
const JSON_RPC_VERSION = '2.0';
public static function success($result, string $id = null)
{
return [
'jsonrpc' => self::JSON_RPC_VERSION,
'result' => $result,
'id' => $id,
];
}
public static function error($error)
{
return [
'jsonrpc' => self::JSON_RPC_VERSION,
'error' => $error,
'id' => null,
];
}
}
Осталось добавить контроллер.
namespace ServerAppHttpControllers;
class DataController extends Controller
{
public function getPageById(array $params)
{
$data = Data::where('page_uid', $params['page_uid'])->first();
return $data;
}
public function create(array $params)
{
$data = DataCreate::create($params);
return $data;
}
}
Не вижу смысла описывать подробно контроллер, вполне стандартные методы. В классе DataCreate
собрана вся логика создания объекта, также проверка на валидность полей с выбросом необходимого исключения.
Вывод
Я старался не усложнять логику самих приложений, а сделать акцент на их взаимодействии.
Про плюсы и минусы JSON-RPC неплохо написано в статье, ссылку на которую я оставлю ниже. Такой подход актуален, например, при реализации встраиваемых форм.
Ссылки
- Подробнее про протокол взаимодействия читаем здесь jsonrpc.org/specification
- REST? Возьмите тупой JSON-RPC статья описывающая плюсы и минусы JSON-RPC
Автор: Сергей Аганисянец