JsonRPC api application with phalcon

Hello,

I need to implement some json-rpc apis in my project, and i thought to use phalcon for this too as it is very nice and powerful tool. So, what's the difference between usual Phalcon\Mvc\Micro examples and JsonRPC? Difference is how controller and action names and request parameters are taken.

For usual applications we take controller, action names from GET and parameters both from GET and POST. For JsonRPC applications we need to grap controller and action names from jsonrpc object, that is placed in raw post data:

For following request we should retrieve controller RobotController, action createAction() and params [type => standard, color => red]

{
  "jsonrpc": "2.0",
  "method": "robot.create", 
  "params": {
    "type"  : "standard",
    "color" : "red"
  }, 
  "id": 1
}

How we can achieve this?

First of all, i looked though Phalcon\Mvc\Router code, docs: http://docs.phalconphp.com/en/latest/api/Phalcon%5Mvc%5Router.html code: https://github.com/phalcon/cphalcon/blob/master/ext/mvc/router.c#L392

Router is responsible for retrieving controller name, action name and parameters. Obviously, this operation is performed in router->handle() method.

Please correct me if i am wrong, router does not use http request object to retrieve controller name, controller action and parameters, it takes them directly from uri, etc. From my point of view, it could be more flexible if router::handle could accept request object with parameters. This way we would be able to inject jsonrpc request with all parameters to router instead of default http request.

For a moment perhaps i am missing something - please correct me if i am wrong. If need to implement such jsonrpc support, do i need to re-define router class and handle() method? It contains lots of code, and i definetely don't want to re-define it.

Perhaps there is another way for easy and painless implementation?



81.1k

You can create a sub-classs of Phalcon\Mvc\Router:

<?php

class JsonRPCRouter extends Phalcon\Mvc\Router
{
    protected $_data;

    public function __construct($data)
    {
        $this->_data = $data;
    }

    public function handle($data=null)
    {

        if (!$data) {
            $data = $this->_data;
        }

        $data = json_decode($data, true);

        if (!isset($data['jsonrpc'])) {
            throw new Phalcon\Mvc\Router\Exception("The request is not Json-RPC");            
        }

        $method = explode('.', $data['method']);

        $this->_controller = $method[0];
        $this->_action = $method[1];
        $this->_params = $data['params'];
    }

}

Stand-alone usage: ```php $r = new JsonRPCRouter(); $r->handle('{"jsonrpc": "2.0","method": "robot.create","params": {"type" : "standard","color" : "red"},"id": 1}'); ``` Register it in the DI:

$di['router'] = function() {
    $data = ///... insert header or raw post data here
    return new JsonRPCRouter($data);
};
edited Oct '14

Thanks a lot for help.

I've added little bit changed code in JsonRPC nfr in github request: https://github.com/phalcon/cphalcon/issues/424

Some more questions regarding json-rpc requests handling:

1) I thought not to echo json response every time inside my controller action.. perhaps it is possible to return some data in action and then to pack this data in JsonRPC object?

For example:

// action context
public function doSmthAction()
{
    $result = true;
    return $result; // this data will be packed automatically to json response and printed in Application class (or similar layer) 
}

2) I've tried to work with Phalcon\Mvc\Micro, but it can't match any route with my controller action. As i understand for this i need to extend your example and to select some route in Router::handle() method, right?

3) I really need controllers (so i suppose i need to work with Phalcon\Mvc\Application), because my api is quite big. But Phalcon\Mvc\Application requires View object. Is it possible to disable it somehow (and enforce Phalcon\Mvc\Application to take value returned by controller action and pack it with JsonRPC\Request)?



81.1k

You can disable the view component this way:

$di['view'] = function() {
    $view = new Phalcon\Mvc\View();
    $view->disable();
    return $view;
};

Rather than echoing the json output you could create a JsonResponse and return it in the action, Phalcon\Mvc\Application automatically will use it as response:

<?php

class Robots extends Phalcon\Mvc\Controller
{

    public function getAction()
    {
        $response = new JsonRPC\Response();
        $response->setContent(array('status' => 'OK', 'message' => 'Everything is alright'));
        return $response;
    }

}

In addition to the Response class you propose, this would handle the response as currently Phalcon does it:

namespace JsonRPC;

class Response extends Phalcon\Http\Response
{
    /**
     * Request id
     * @var string|int|null
     */
    public $id;

    /**
     * Request version
     * @var string
     */
    public $version = '2.0';

    /**
     * Method execution result
     * @var string
     */
    public $result;

    /**
     * Error occured while executing
     * JsonRPC request
     * @var JsonRPC\Exception
     */
    public $error;

    /**
     * Returns string representation
     * @return string
     */
    public function getContent()
    {
        $response = [
            'id'      => $this->id,
            'jsonrpc' => $this->version
        ];

        //Use the current content
        $result = parent::getContent();

        if (isset($this->error)) {
            $response['error'] = [
                'code'    => $this->error->getCode(),
                'message' => $this->error->getMessage(),
            ];
        } else {
            $response['result'] = $result;
        }

        return json_encode($response);
    }
}

Woow, thank you a lot for such detailed response!

One more question regarding jsonrpc: is it possible to perform multi-tasking application? For example, i can send a list of json requests to the server and i should get a list of responses, this is possible in the specification: http://www.jsonrpc.org/specification#request%5object (look at p.6, "Batch")

I'm little bit lost how to make it correct, first thing i thought about: 1) parse jsonrpc requests list 2) for each jsonrpc request re-create DI and Phalcon\Mvc\Application and execute controller action. But it seems little bit crazy solution.

Second idea was to implement everything using 1 DI object and 1 application for everything. But there is an issue - we need to execute routes in a loop (to execute all jsonrpc requests batch), Is it possible to execute not a single route, but routes list?

Really, actions should not change many things in DI environment, so idea to make everything in one application object looks more attractive for me. Is it enough to re-define Phalcon\Mvc\Application::handle() method for this?

It could look like this.. what do you think?

// 1. Make custom Http\Request class, that extends Phalcon\Http\Request

class Request extends Phalcon\Http\Request
{
    /**
     * Rpc requests
     * @var JsonRPC\Request[]
     */
    protected $rpcRequests = [];

    /**
     * Constructor
     * @return void
     */
    public function __construct()
    {
        // creates rpc requests collection from raw body
        // and adds them to $this->rpcRequests
    }
}

// 2. Make custom RPC\Router (that probably extends Phalcon\Mvc\Router)

class Router extends Phalcon\Mvc\Router
{
    public function handle(JsonRPC\Request $request)
    {
        $this->_controller = $request->controller;
        $this->_action = $request->action;
        $this->_params = $request->params;
    }
}

// 3. Make custom Application class with custom handle method

class Application
{
    public function handle()
    {
        $di       = $this->getDI();
        $requests = $di->getRequest()->getRpcRequests();
        $router   = $di->getRouter();

        foreach($requests as $request) {

            // Prepare response object
            $response = new JsonRPC\Response();
            $response->tid     = $request->tid;
            $response->type    = $request->type;
            $response->action  = $request->action;
            $response->method  = $request->method;
            $response->success = false;

            try {

                $router->handle($request);
                $dispatcher->dispatch();
                $response->success = true;
                $response->result  = $dispatcher->getReturnedValue();;

            } catch (\Exception $e) {

                $response->messages = [[
                    'code' => $e->getCode(),
                    'text' => $e->getMessage(),
                ]];
            }
        }
    }

}

P.S. As i understood from dispatcher source, even router here is not necessary, we can directly use dispatcher:

$dispatcher = $di->getShared('dispatcher');
foreach($rpcRequests as $request) {
    $dispatcher->setControllerName($request->controller);
    $dispatcher->setActionName($request->action);
    $dispatcher->setParams($request->parameters);
}

Right?



81.1k

This MVC skeleton (https://github.com/phalcon/mvc/blob/master/simple-without-application/public/index.php) doesn't use Phalcon\Mvc\Application, it could give you more freedom to adapt the code to your needs

I see, thanks a lot :)

One thing i can't get there - can i throw away router from this scheme - as i understand router role is to parse url and perform some checks that url matches some route and to retrieve controller and action name.

If i know controller and action names, and if i can setup them directly in dispatcher, can i use dispatcher directly? Or there are some internal dependencies in other components (like dispatcher), that are based on router and i have to implement it in my application anyway?



81.1k

Yes, Dispatcher could be used directly without a router, also removing Phalcon\Mvc\Application also removes the 'view' component which is unnecessary here too.