Implement JSON-RPC with zend-json-server
zend-json-server1 provides a JSON-RPC2 implementation. JSON-RPC is similar
to XML-RPC or SOAP in that it implements a Remote Procedure Call server at a
single URI using a predictable calling semantic. Like each of these other
protocols, it provides the ability to introspect the server in order to
determine what calls are available, what arguments each call expects, and the
expected return value(s); JSON-RPC implements this via a Service Mapping
Description (SMD)3, which is usually available via an HTTP GET
request to
the server.
zend-json-server was designed to work standalone, allowing you to map a URL to a specific script that then handles the request:
$server = new Zend\Json\Server\Server();
$server->setClass('Calculator');
// SMD request
if ('GET' === $_SERVER['REQUEST_METHOD']) {
// Indicate the URL endpoint, and the JSON-RPC version used:
$server->setTarget('/json-rpc')
->setEnvelope(Zend\Json\Server\Smd::ENV_JSONRPC_2);
// Grab the SMD
$smd = $server->getServiceMap();
// Return the SMD to the client
header('Content-Type: application/json');
echo $smd;
return;
}
// Normal request
$server->handle();
What the above example does is:
- Create a server.
- Attach a class or object to the server. The server introspects that class in order to expose any public methods on it as calls on the server itself.
- If an HTTP
GET
request occurs, we present the service mapping description. - Otherwise, we attempt to handle the request.
All server components in Zend Framework work similar to the above. Introspection via function or class reflection allows quickly creating and exposing services via these servers, as well as enables the servers to provide SMD, WSDL, or XML-RPC system information.
However, this approach can lead to difficulties:
- What if I need access to other application services? or want to use the fully-configured application dependency injection container?
- What if I want to be able to control the URI via a router?
- What if I want to be able to add authentication or authorization in front of the server?
In other words, how do I use the JSON-RPC server as part of a larger application?
Below, I'll outline using zend-json-server in both a Zend Framework MVC
application, as well as via PSR-7 middleware. In both cases, you may assume that
Acme\ServiceModel
is a class exposing public methods we wish to expose via the
server.
Using zend-json-server within zend-mvc
To use zend-json-server within a zend-mvc application, you will need to:
- Provide a
Zend\Json\Server\Response
instance to theServer
instance. - Tell the
Server
instance to return the response. - Populate the MVC's response from the
Server
's response. - Return the MVC response (which will short-circuit the view layer).
This third step requires a bit of logic, as the default response type,
Zend\Json\Server\Response\Http
, does some logic around setting headers that
you'll need to duplicate.
A full example will look like the following:
namespace Acme\Controller;
use Acme\ServiceModel;
use Zend\Json\Server\Response as JsonResponse;
use Zend\Json\Server\Server as JsonServer;
use Zend\Mvc\Controller\AbstractActionController;
class JsonRpcController extends AbstractActionController
{
private $model;
public function __construct(ServiceModel $model)
{
$this->model = $model;
}
public function endpointAction()
{
$server = new JsonServer();
$server
->setClass($this->model)
->setResponse(new JsonResponse())
->setReturnResponse();
/** @var JsonResponse $jsonRpcResponse */
$jsonRpcResponse = $server->handle();
/** @var \Zend\Http\Response $response */
$response = $this->getResponse();
// Do we have an empty response?
if (! $jsonRpcResponse->isError()
&& null === $jsonRpcResponse->getId()
) {
$response->setStatusCode(204);
return $response;
}
// Set the content-type
$contentType = 'application/json-rpc';
if (null !== ($smd = $jsonRpcResponse->getServiceMap())) {
// SMD is being returned; use alternate content type, if present
$contentType = $smd->getContentType() ?: $contentType;
}
// Set the headers and content
$response->getHeaders()->addHeaderLine('Content-Type', $contentType);
$response->setContent($jsonRpcResponse->toJson());
return $response;
}
}
Inject your dependencies!
You'll note that the above example accepts the
Acme\ServiceModel
instance via its constructor. This means that you will need to provide a factory for your controller, to ensure that it is injected with a fully configured instance — and that likely also means a factory for the model, too.To simplify this, you may want to check out the ConfigAbstractFactory4 or ReflectionBasedAbstractFactory5, both of which were introduced in version 3.2.0 of zend-servicemanager.
Using zend-json-server within PSR-7 middleware
Using zend-json-server within PSR-7 middleware is similar to zend-mvc:
- Provide a
Zend\Json\Server\Response
instance to theServer
instance. - Tell the
Server
instance to return the response. - Create and return a PSR-7 response based on the
Server
's response.
The code ends up looking like the following:
namespace Acme\Controller;
use Acme\ServiceModel;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\EmptyResponse;
use Zend\Diactoros\Response\TextResponse;
use Zend\Json\Server\Response as JsonResponse;
use Zend\Json\Server\Server as JsonServer;
class JsonRpcMiddleware
{
private $model;
public function __construct(ServiceModel $model)
{
$this->model = $model;
}
public function __invoke(
ServerRequestInterface $request,
ResponseInterface $response,
callable $next
) {
$server = new JsonServer();
$server
->setClass($this->model)
->setResponse(new JsonResponse())
->setReturnResponse();
/** @var JsonResponse $jsonRpcResponse */
$jsonRpcResponse = $server->handle();
// Do we have an empty response?
if (! $jsonRpcResponse->isError()
&& null === $jsonRpcResponse->getId()
) {
return new EmptyResponse();
}
// Get the content-type
$contentType = 'application/json-rpc';
if (null !== ($smd = $jsonRpcResponse->getServiceMap())) {
// SMD is being returned; use alternate content type, if present
$contentType = $smd->getContentType() ?: $contentType;
}
return new TextResponse(
$jsonRpcResponse->toJson(),
200,
['Content-Type' => $contentType]
);
}
}
In the above example, I use a couple of zend-diactoros6-specific
response types to ensure that we have no extraneous information in the returned
responses. I use TextResponse
specifically, as the toJson()
method on the
zend-json-server response returns the actual JSON string, versus a data
structure that can be cast to JSON.
Per the note above, you will need to configure your dependency injection container to inject the middleware instance with the model.
Summary
zend-json-server provides a flexible, robust, and simple way to create JSON-RPC services. The design of the component makes it possible to use it standalone, or within any application framework you might be using. Hopefully the examples above will aid you in adapting it for use within your own application!
Visit the zend-json-server documentation7 to find out what else you might be able to do with this component!
Footnotes
1. https://docs.zendframework.com/zend-json-server/ ↩
2. http://groups.google.com/group/json-rpc/ ↩
3. http://www.jsonrpc.org/specification ↩
4. https://docs.zendframework.com/zend-servicemanager/config-abstract-factory/ ↩
5. https://docs.zendframework.com/zend-servicemanager/reflection-abstract-factory/ ↩
6. https://docs.zendframework.com/zend-diactoros ↩
7. https://docs.zendframework.com/zend-json-server/ ↩