Apigility: Using RPC with HAL
A few days ago, we released our first beta of Apigility. We've started our documentation effort now, and one question has arisen a few times that I want to address: How can you use Hypermedia Application Language (HAL) in RPC services?
HAL?
Hypermedia Application Language
is an IETF proposal for how to represent resources and their relations within
APIs. Technically, it provides two mediatypes, application/hal+json
and
application/hal+xml
; however, Apigility only provides the JSON variant.
The important things to know about HAL are:
-
It provides a standard way of describing relational links. All relational links are under a
_links
property of the resource. That property is an object. Each property of that object is a link relation; the value of each link relation is an object (or array of such objects) describing the link that must minimally contain anhref
proerty. The link object itself can contain some additional metadata, such as a mediatype, a name (useful for differentiating between multiple link objects assigned to the same relation).While not required, the specification recommends resources contain a "self" relational link, indicating the canonical location for the resource. This is particularly useful when we consider embedding (the next topic).
Sound hard? It's not:
{ "_links": { "self": { "href": "/blog/2014-03-26-apigility-rpc-with-hal" } } }
-
Besides link relations, HAL also provides a standard way of describing embedded resources. An embedded resource is any other resource you can address via your API, and, as such, would be structured as a HAL resource — in other words, it would have a
_links
property with relational links. Essentially, any property of the resource you're returning that can itself be addressed via the URI must be embedded in the resource. This is done via the property_embedded
.Like
_links
,_embedded
is an object. Each key in the object is the local name by which the resource refers to the embedded resource. The value of such keys can either be HAL resources or arrays of HAL resources; in fact, this is how collections are represented in HAL!As examples:
{ "_links": { "self": { "href": "/blog/2014-03-26-apigility-rpc-with-hal" } }, "_embedded": { "author": { "_links": { "self": { "href": "/blog/author/matthew" } }, "id": "matthew", "name": "Matthew Weier O'Phinney", "url": "http://mwop.net" }, "tags": [ { "_links": { "self": { "href": "/blog/tag/php" } }, "id": "php" }, { "_links": { "self": { "href": "/blog/tag/rest" } }, "id": "rest" } ] } }
The example above shows two embedded resources. The first is the author; the second, a collection of tags. Note that every object under
_embedded
is a HAL object!You can go quite far with this — you can also have embedded resources inside your embedded resources, arbitrarily deep.
RPC?
RPC stands for Remote Procedure Call, and, when describing a web API, is
usually used to describe a web service that publishes multiple method calls at
a single URI using only POST
; XML-RPC and SOAP are the usual suspects.
In Apigility, we use the term RPC in a much looser sense; we use it to describe one-off services: actions like "authenticate," or "notify," or "register" would all make sense here. They are actions that usually only need to respond to a single HTTP method, and which may or may not describe a "thing", which is what we usually consider a "resource" when discussing REST terminology.
That said: what if what we want to return from the RPC call are REST resources?
Returning HAL from RPC Services
In order to return HAL from RPC services, we need to understand (a) how Content Negotiation works, and (b) what needs to be returned in order for the HAL renderer to be able to create a representation.
For purposes of this example, I'm positing a RegisterController
as an RPC
service that, on success, is returning a User
object that I want rendered as
a HAL resource.
The zf-content-negotiation
module takes care of content negotiation for Apigility. It introspects the
Accept
header in order to determine if we can return a representation, and
then, if it can, will cast any ZF\ContentNegotiation\ViewModel
returned from
a controller to the appropriate view model for the representation. From there,
a renderer will pick up the view model and do what needs to be done.
So, the first thing we have to do is return ZF\ContentNegotiation\ViewModel
instances from our controller.
use Zend\Mvc\Controller\AbstractActionController;
use ZF\ContentNegotiation\ViewModel;
class RegisterController extends AbstractActionController
{
public function registerAction()
{
/* ... do some work ... get a user ... */
return new ViewModel(array('user' => $user));
}
}
The zf-hal module in Apigility creates
the actual HAL representations. zf-hal
looks for a "payload" variable in the
view model, and expects that value to be either a ZF\Hal\Entity
(single item)
or ZF\Hal\Collection
. When creating an Entity
object, you need the object
being represented, as well as the identifier. So, let's update our return
value.
use Zend\Mvc\Controller\AbstractActionController;
use ZF\ContentNegotiation\ViewModel;
use ZF\Hal\Entity;
class RegisterController extends AbstractActionController
{
public function registerAction()
{
/* ... do some work
* ... get a $user
* ... assume we have also now have an $id
*/
return new ViewModel(array('payload' => array(
'user' => new Entity($user, $id),
)));
}
}
zf-hal
contains what's called a "metadata map". This is a map of classes to
information on how zf-hal
should render them: what route to use, what
additional relational links to inject, how to serialize the object, what field
represents the identifier, etc.
In most cases, you will have likely already defined a REST service for the
resource you want to return from the RPC service, in which case you will be
done. However, if you want, you can go in and manually configure the metadata
map in your API module's config/module.config.php
file:
return array(
/* ... */
'zf-hal' => array(
'metadata_map' => array(
'User' => array(
'route_name' => 'api.rest.user',
'entity_identifier_name' => 'username',
'route_identifier_name' => 'user_id',
'hydrator' => 'Zend\Stdlib\Hydrator\ObjectProperty',
),
),
),
);
Finally, we need to make sure that the service is configured to actually return
HAL. We can do this in the admin if we want. Find the "Content Negotiation"
section of the admin, and the "Content Negotiation Selector" item, and set that
to "HalJson"; don't forget to save! Alternately, you can do this manually in
the API module's config/module.config.php
file, under the
zf-content-negotiation
section:
return array(
/* ... */
'zf-content-negotiation' => array(
'controllers' => array(
/* ... */
'RegisterController' => 'HalJson',
),
/* ... */
),
);
Once your changes are complete, when you make a successful request to the URI for your "register" RPC service, you'll receive a HAL response pointing to the canonical URI for the user resource created!