SOA на Laravel и JSON-RPC 2.0

в 10:22, , рубрики: laravel, php, php laravel soa, Программирование, Проектирование и рефакторинг

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 неплохо написано в статье, ссылку на которую я оставлю ниже. Такой подход актуален, например, при реализации встраиваемых форм.

Ссылки

Автор: Сергей Аганисянец

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js