A PHP library to support implementing representations for HATEOAS REST web services.
Using Composer, just require the willdurand/hateoas
package:
{
"require": {
"willdurand/hateoas": "@stable"
}
}
Otherwise, install the library and setup the autoloader yourself.
This library is under heavy development but basically it's a wrapper to add hypermedia links to a resource or a collection of resources.
First of all, you have to wrap your data in a Resource
object:
<?php
$resource = new Resource(array('foo' => 'bar'));
// or
$resource = new Resource($user);
Now, you are able to add links to this resource:
<?php
$resource->addLink(new Link('http://example.com/users/999', Link::REL_SELF));
$resource->addLink(new Link('http://example.com/users/999/friends', 'friends', 'application/vnd.acme.users'));
To sum up, a Resource
contains data and links related to the data.
This library also provides a RouteAwareLinkBuilder
which relies on a
UrlGeneratorInterface
instance under the hood. In Symfony2, you could use the
router
service as shown in the following example, but this library is not
tied to Symfony2.
<?php
// if you want to use the Symfony2 router in a Symfony2 project
$linkBuilder = new RouteAwareLinkBuilder($this->get('router'));
// in a Silex project
$linkBuilder = new RouteAwareLinkBuilder($app['url_generator']);
// Generate a "self" link
$selfLink = $linkBuilder->create('user_get', array('id' => $user->getId()), Link::REL_SELF);
$resource->addLink($selfLink);
The RouteAwareLinkBuilder
has been described above. This builder uses the
Symfony2 Router, but what if you don't use it? CallableLinkBuilder
to the
rescue! This builder takes a callable
as argument:
<?php
use Hateoas\Builder\CallableLinkBuilder;
$linkBuilder = new CallableLinkBuilder(function ($route, $parameters) use ($myRouter) {
return $myRouter->generateRoute($route, $parameters);
});
Hateoas uses the JMS Serializer, and
hooks into it to provide nice features, but that also means you need to use
a configured serializer. Hateoas provides a configured serializer through
Hateoas::getSerializer()
.
If you don't want to use this configured serializer, be sure to enable
annotations support, and to register the Handler
bundled with Hateoas.
Also, be sure to enable annotations as Hateoas uses them. The fastest way to activate annotations is to add the following line somewhere near your autoloader configuration:
<?php
Doctrine\Common\Annotations\AnnotationRegistry::registerLoader('class_exists');
Hateoas provides factories and builders to generate Resource
and Link
instances. A Factory takes a ConfigInterface
object as argument.
That means you can use XML, YAML, annotations, etc. even if it's not yet
implemented in the library itself.
At the moment, Hateoas is bundled with an ArrayConfig
and a YamlConfig
which relies on the Symfony2 Yaml component under the hood.
The ArrayConfig
class takes two arrays as arguments.
The YamlConfig
class takes either a filename or a string containing a YAML
configuration as described below:
hateoas:
resources:
Acme\Entity\Location:
links:
- { route: 'location.get', parameters: [ 'id' ], rel: 'self', type: 'application/vnd.acme.location' }
- { route: 'location.get_comments', parameters: [ 'id' ], rel: 'comments', type: 'application/vnd.acme.comments' }
embeds:
- { name: 'last-comments', accessor: 'lastComments'}
Acme\Entity\Comment:
links:
- { route: 'comment.get', parameters: [ 'id' ], rel: 'self', type: 'application/vnd.acme.comment' }
collections:
Acme\Entity\Location:
rootName: locations
links:
- { route: 'location.all', rel: 'self', type: 'application/vnd.acme.locations' }
Acme\Entity\Comment:
rootName: comments
links:
- { route: 'comment.all', rel: 'self', type: 'application/vnd.acme.comments' }
In order to describe a Link
, you need to define a rel
attribute (and
optionally a type
). If you are using Symfony2, you can describe a
RouteLinkDefinition
so that you can define a route
and its parameters
:
<?php
$linkDefinition = array(
'route' => 'acme_demo.user_get',
'parameters' => array('id'),
'rel' => Link::REL_SELF,
'type' => null
);
// or
$linkDefinition = new RouteLinkDefinition('acme_demo.user_get', array('id'), Link::REL_SELF);
Note: you can use the
RouteLinkDefinition
even if you don't use the Symfony2 Router. It can be useful if your own router relies on the same principle (a route and its parameters).
You can load embeds
resources from within a defined resource. You need to
define a name
for the collection you are embedding, and optionally an
accessor
to load it from the parent resource.
<?php
$embedDefinition = array(
'name' => 'comments',
'accessor' => 'TopComments'
);
//or
$embedDefinition = new EmbedDefinition('comments', 'TopComments');
Note: The embed resources are rendered as a sequence of individual resources, not as a collection.
Now, you need a factory. Symfony2 users will be interested in the
RouteAwareFactory
, others can implement their own Factory:
<?php
use Hateoas\Factory\RouteAwareFactory;
$factory = new RouteAwareFactory(
new ArrayConfig(
array(
'Acme\DemoBundle\Model\User' => array(
'links' => array(
$linkDefinition,
array(
'route' => 'acme_demo.friend_get',
'parameters' => array('id'),
'rel' => 'friends',
'type' => 'application/vnd.acme.users'
)
)
)
)
)
);
This factory allows to create a ResourceDefinition
by taking either an
instance or a classname. This definition contains a class name and a set of
LinkDefinition
. The RouteAwareFactory
described above allows to create
RouteLinkDefinition
instances.
Now, you probably want to create resources using your configuration. Thanks to
the ResourceBuilder
it's super easy. A ResourceBuilder
needs a
RouteAwareLinkBuilder
and a Factory
:
<?php
use Hateoas\Builder\ResourceBuilder;
$resourceBuilder = new ResourceBuilder($factory, $linkBuilder);
Now, you can create a resource for a given object:
<?php
$resource = $resourceBuilder->create($user);
$resource
is an instance of Resource
and contains two Link
(self
and
friends
).
You need to pass a configuration array for your collections as second argument
of your Factory
:
<?php
use Hateoas\Factory\RouteAwareFactory;
$factory = new RouteAwareFactory(
new ArrayConfig(
// single resource
array(
'Acme\DemoBundle\Model\User' => array(
'links' => array(
$linkDefinition,
array(
'route' => 'acme_demo.friend_get',
'parameters' => array('id'),
'rel' => 'friends',
'type' => 'application/vnd.acme.users'
)
),
'embed' => array(
$embedDefinition,
array('name' => 'best_friends', 'accessor' => 'BestFriends')
)
)
),
// collection
array(
'Acme\DemoBundle\Model\User' => array(
'links' => array(
array(
'route' => 'acme_demo.user_all',
'rel' => Link::REL_SELF,
'type' => 'application/vnd.acme.users'
),
array(
'route' => 'acme_demo.user_all',
'parameters' => array('page'),
'rel' => Link::REL_NEXT
),
),
),
)
)
);
Then, you just have to call the createCollection()
method on the
ResourceBuilder
:
<?php
$collection = $resourceBuilder->createCollection(
array($user1, $user2, ...),
'Acme\DemoBundle\Model\User'
);
A Collection
contains a set of resources, links related to these
resources, and metadata about the collection. For instance, you can set the
rootName
that is the name of the set of resources.
If you use pagination, you can set the number of resources (total
), the
current page
, and the limit
.
Both methods create()
and createCollection()
accept an optional parameter to
define child properties to iterate over. For example you have a Post with a
author
property.
With the following code it adds also hyperlinks to the author
object:
<?php
$resource = $resourceBuilder->create($user, array('objectProperties' => array('author')));
Let's say you have an application that manages a set of locations, and each location owns a set of comments. According to the YAML configuration shown above, here are the outputs:
<?xml version="1.0" encoding="UTF-8"?>
<locations>
<link href="http://localhost:8081/locations" rel="self" type="application/vnd.example.locations"/>
<location>
<link href="http://localhost:8081/locations/1" rel="self" type="application/vnd.example.location"/>
<link href="http://localhost:8081/locations/1/comments" rel="comments" type="application/vnd.example.comments"/>
<id><![CDATA[1]]></id>
<name><![CDATA[test]]></name>
<created_at><![CDATA[2013-02-07T18:37:03+0100]]></created_at>
</location>
<location>
<link href="http://localhost:8081/locations/4" rel="self" type="application/vnd.example.location"/>
<link href="http://localhost:8081/locations/4/comments" rel="comments" type="application/vnd.example.comments"/>
<id><![CDATA[4]]></id>
<name><![CDATA[foobar]]></name>
<created_at><![CDATA[2013-02-07T18:37:04+0100]]></created_at>
</location>
</locations>
{
"locations" : [
{
"_links" : {
"self": {
"href" : "http://localhost:8081/locations/1",
"type" : "application/vnd.example.location"
},
"comments": {
"href" : "http://localhost:8081/locations/1/comments",
"type" : "application/vnd.example.comments"
}
},
"id" : "1",
"name" : "test",
"created_at" : "2013-02-07T18:37:03+0100",
},
{
"_links" : {
"self": {
"href" : "http://localhost:8081/locations/4",
"type" : "application/vnd.example.location"
},
"comments": {
"href" : "http://localhost:8081/locations/4/comments",
"type" : "application/vnd.example.comments"
}
},
"id" : "4",
"name" : "foobar",
"created_at" : "2013-02-07T18:37:04+0100"
}
],
"_links" : {
"self": {
"rel" : "self",
"href" : "http://localhost:8081/locations",
"type" : "application/vnd.example.locations"
}
}
}
The JSON representation of links follows the Hypertext Application Language specification.
<?xml version="1.0" encoding="UTF-8"?>
<location>
<link href="http://localhost:8081/locations/4" rel="self" type="application/vnd.example.location"/>
<link href="http://localhost:8081/locations/4/comments" rel="comments" type="application/vnd.example.comments"/>
<id><![CDATA[4]]></id>
<name><![CDATA[foobar]]></name>
<created_at><![CDATA[2013-02-07T18:37:04+0100]]></created_at>
</location>
{
"_links" : {
"self": {
"href" : "http://localhost:8081/locations/4",
"type" : "application/vnd.example.location"
},
"comments": {
"href" : "http://localhost:8081/locations/4/comments",
"type" : "application/vnd.example.comments"
}
},
"id" : "4",
"name" : "foobar",
"created_at" : "2013-02-07T18:37:04+0100"
}
<?xml version="1.0" encoding="UTF-8"?>
<comments>
<link href="http://localhost:8081/comments" rel="self" type="application/vnd.example.comments"/>
<comment>
<link href="http://localhost:8081/comments/8" rel="self" type="application/vnd.example.comment"/>
<id><![CDATA[8]]></id>
<username><![CDATA[anonymous]]></username>
<body><![CDATA[]]></body>
<created_at><![CDATA[2013-02-07T19:41:14+0100]]></created_at>
</comment>
</comments>
{
"comments" : [
{
"_links" : {
"self": {
"href" : "http://localhost:8081/comments/8",
"type" : "application/vnd.example.comment"
}
},
"id" : "8",
"username" : "anonymous",
"body" : "",
"created_at" : "2013-02-07T19:41:14+0100"
},
],
"_links" : {
"self": {
"href" : "http://localhost:8081/comments",
"type" : "application/vnd.example.comments"
}
}
}
Let's say you have a pager like the Propel Pager, you can configure a set of links for your collection:
$factory = new RouteAwareFactory(
new ArrayConfig(
// single resource
array(
'Acme\DemoBundle\Model\User' => array(
$linkDefinition,
array(
'route' => 'acme_demo.friend_get',
'parameters' => array('id'),
'rel' => 'friends',
'type' => 'application/vnd.acme.users'
),
),
),
// collection
array(
'Acme\DemoBundle\Model\User' => array(
'links' => array(
array(
'route' => 'acme_demo.user_all',
'parameters' => array('page'),
'rel' => Link::REL_SELF,
'type' => 'application/vnd.acme.users'
),
array(
'route' => 'acme_demo.user_all',
'parameters' => array('page' => 'firstPage'),
'rel' => Link::REL_FIRST,
'type' => 'application/vnd.acme.users'
),
array(
'route' => 'acme_demo.user_all',
'parameters' => array('page' => 'lastPage'),
'rel' => Link::REL_LAST,
'type' => 'application/vnd.acme.users'
),
array(
'route' => 'acme_demo.user_all',
'parameters' => array('page' => 'nextPage'),
'rel' => Link::REL_NEXT,
'type' => 'application/vnd.acme.users'
),
array(
'route' => 'acme_demo.user_all',
'parameters' => array('page' => 'previousPage'),
'rel' => Link::REL_PREVIOUS,
'type' => 'application/vnd.acme.users'
),
),
'attributes' => array(
'page' => 'page',
'limit' => 'maxPerPage',
'total' => 'nbResults',
)
),
)
)
);
Then, do:
<?php
$collection = $resourceBuilder->createCollection(
UserQuery::create()->paginate(), // returns an instance of ModelPager
'Acme\DemoBundle\Model\User'
);
You will get the following output:
{
"total": 1000,
"page": 1,
"limit": 10,
"resources": [
{
"id": 999,
"username": "xxxx",
"email": "[email protected]",
"_links": {
"self": {
"href": "http://example.com/users/999"
}
}
},
// ...
],
"_links": {
"self": {
"href": "http://example.com/users?page=1",
"type": "application/vnd.acme.users"
},
"previous": {
"href": "http://example.com/users?page=1",
"type":"application/vnd.acme.users"
},
"next": {
"href": "http://example.com/users?page=2",
"type":"application/vnd.acme.users"
},
"first": {
"href": "http://example.com/users?page=1",
"type":"application/vnd.acme.users"
},
"last": {
"href": "http://example.com/users?page=100",
"type":"application/vnd.acme.users"
}
}
}
Hateoas is released under the MIT License. See the bundled LICENSE file for details.