SOA on Laravel and JSON-RPC 2.0

SOA (Service Oriented Architecture) is built by combining and interacting loosely coupled services.

To demonstrate, we will create two applications, Client and Server, and organize their interaction using the remote procedure call protocol JSON-RPC 2.0.

Customer


The Client application is a site for creating and displaying certain content. The client does not contain its own database, but receives and adds data through interaction with the Server application.

On the client, the interaction provides a classJsonRpcClient

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

We need a library GuzzleHttp, pre-install it.
We form a completely standard POSTrequest using GuzzleHttp\Client. The main caveat here is the request format.
According to the specification, the JSON-RPC 2.0request should look like:

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

  • jsonrpc protocol version, must indicate "2.0"
  • method method name
  • params array with parameters
  • id request id

Answer

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

If the request was completed with an error, we get

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

  • jsonrpc protocol version, must indicate "2.0"
  • resultrequired field for a successful query result. Should not exist when an error occurs
  • errorrequired field when an error occurs. Should not exist on successful outcome
  • id request identifier set by client

The server forms the answer, so we will come back to it.

In the controller, it is necessary to form a request with the necessary parameters and process the response.

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

The JSON-RPC fixed response format makes it easy to see if the request was successful and take any action if the response contains an error.

Server


Let's start by setting up routing. In the file routes/api.phpadd

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

All requests received by the server at the address <server_base_uri>/datawill be processed by the classJsonRpcServer

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

The class JsonRpcServerbinds the desired controller method with the passed parameters. And it returns the response generated by the class JsonRpcResponsein the format according to the specification JSON-RPC 2.0described above.

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

It remains to add a controller.

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

I see no reason to describe the controller in detail, quite standard methods. The class DataCreatecontains all the logic of creating an object, as well as checking for the validity of fields with the throwing of the necessary exception.

Conclusion


I tried not to complicate the logic of the applications themselves, but to focus on their interaction.
The pros and cons of JSON-RPC are well written in an article, a link to which I will leave below. This approach is relevant, for example, when implementing embedded forms.

References



All Articles