SOA no Laravel e JSON-RPC 2.0

A SOA (Arquitetura Orientada a Serviços) é criada combinando e interagindo serviços de acoplamento fraco.

Para demonstrar, criaremos dois aplicativos, Cliente e Servidor, e organizaremos sua interação usando o protocolo de chamada de procedimento remoto JSON-RPC 2.0.

Cliente


O aplicativo cliente é um site para criar e exibir determinado conteúdo. O cliente não contém seu próprio banco de dados, mas recebe e adiciona dados devido à interação com o aplicativo Servidor.

No cliente, a interação fornece uma classeJsonRpcClient

namespace ClientApp\Services;

use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;

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);
    }
}

Precisamos de uma biblioteca GuzzleHttp, pré-instale-a.
Formamos uma POSTsolicitação completamente padrão usando GuzzleHttp\Client. A principal ressalva aqui é o formato de solicitação.
De acordo com a especificação, a JSON-RPC 2.0solicitação deve se parecer com:

{
    "jsonrpc": "2.0", 
    "method": "getPageById",
    "params": {
        "page_uid": "f09f7c040131"
    }, 
    "id": "54645"
}

  • jsonrpc versão do protocolo, deve indicar "2.0"
  • method nome do método
  • params matriz com parâmetros
  • id Identificação do Pedido

Responda

{
    "jsonrpc": "2.0",
    "result": {
        "id": 2,
        "title": "Index Page",
        "content": "Content",
        "description": "Description",
        "page_uid": "f09f7c040131"
    },
    "id": "54645"
}

Se a solicitação foi concluída com um erro, obtemos

{
    "jsonrpc": "2.0",
    "error": {
        "code": -32700,
        "message": "Parse error"
    },
    "id": "null"
}

  • jsonrpc versão do protocolo, deve indicar "2.0"
  • resultcampo obrigatório para um resultado de consulta bem-sucedido. Não deveria existir quando ocorre um erro
  • errorcampo obrigatório quando ocorrer um erro. Não deve existir em resultado bem sucedido
  • id identificador de solicitação definido pelo cliente

O servidor forma a resposta, então retornaremos a ela.

No controlador, é necessário formar uma solicitação com os parâmetros necessários e processar a resposta.

namespace ClientApp\Http\Controllers;

use App\Services\JsonRpcClient;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;

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']]);
    }
}

O formato de resposta fixa JSON-RPC facilita ver se a solicitação foi bem-sucedida e executar qualquer ação se a resposta contiver um erro.

Servidor


Vamos começar configurando o roteamento. No arquivo, routes/api.phpadicione

Route::post('/data', function (Request $request, JsonRpcServer $server, DataController $controller) {
    return $server->handle($request, $controller);
});

Todas as solicitações recebidas pelo servidor no endereço <server_base_uri>/dataserão processadas pela classeJsonRpcServer

namespace ServerApp\Services;

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());
        }
    }
}

A classe JsonRpcServerliga o método do controlador desejado com os parâmetros passados. E retorna a resposta gerada pela classe JsonRpcResponseno formato de acordo com a especificação JSON-RPC 2.0descrita acima.

use ServerApp\Http\Response;

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,
        ];
    }
}

Resta adicionar um controlador.

namespace ServerApp\Http\Controllers;

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;
    }
}

Não vejo razão para descrever o controlador em detalhes, métodos bastante padrão. A classe DataCreatecontém toda a lógica de criação de um objeto, além de verificar a validade dos campos com o lançamento da exceção necessária.

Conclusão


Tentei não complicar a lógica dos aplicativos, mas focar na interação deles.
Os prós e contras do JSON-RPC estão bem escritos em um artigo, um link ao qual deixarei abaixo. Essa abordagem é relevante, por exemplo, ao implementar formulários incorporados.

Referências



All Articles