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.user'));
This library also provides a LinkBuilder
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 LinkBuilder($this->get('router'));
// in a Silex project
$linkBuilder = new LinkBuilder($app['url_generator']);
// Generate a "self" link
$selfLink = $linkBuilder->create('user_get', array('id' => $user->getId()), Link::REL_SELF);
$resource->addLink($selfLink);
The LinkBuilder
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:
- { route: 'location.get', parameters: [ 'id' ], rel: 'self', type: 'application/vnd.acme.location' }
- { route: 'location.get_comments', parameters: [ 'id' ], rel: 'comments', type: 'application/vnd.acme.comment' }
Acme\Entity\Comment:
- { route: 'comment.get', parameters: [ 'id' ], rel: 'self', type: 'application/vnd.acme.comment' }
collections:
Acme\Entity\Location:
links:
- { route: 'location.all', rel: 'self', type: 'application/vnd.acme.location' }
Acme\Entity\Comment:
links:
- { route: 'comment.all', rel: 'self', type: 'application/vnd.acme.comment' }
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).
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(
$linkDefinition,
array(
'route' => 'acme_demo.friend_get',
'parameters' => array('id'),
'rel' => 'friends',
'type' => 'application/vnd.acme.user'
),
),
)
)
);
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 LinkBuilder
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
10000
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(
$linkDefinition,
array(
'route' => 'acme_demo.friend_get',
'parameters' => array('id'),
'rel' => 'friends',
'type' => 'application/vnd.acme.user'
),
),
),
// collection
array(
'Acme\DemoBundle\Model\User' => 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'
);
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"?>
<resources>
<link href="http://localhost:8081/locations" rel="self" type="application/vnd.example.location"/>
<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.comment"/>
<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.comment"/>
<id><![CDATA[4]]></id>
<name><![CDATA[foobar]]></name>
<created_at><![CDATA[2013-02-07T18:37:04+0100]]></created_at>
</location>
</resources>
{
"resources" : [
{
"created_at" : "2013-02-07T18:37:03+0100",
"_links" : [
{
"rel" : "self",
"href" : "http://localhost:8081/locations/1",
"type" : "application/vnd.example.location"
},
{
"rel" : "comments",
"href" : "http://localhost:8081/locations/1/comments",
"type" : "application/vnd.example.comment"
}
],
"name" : "test",
"id" : "1"
},
{
"created_at" : "2013-02-07T18:37:04+0100",
"_links" : [
{
"rel" : "self",
"href" : "http://localhost:8081/locations/4",
"type" : "application/vnd.example.location"
},
{
"rel" : "comments",
"href" : "http://localhost:8081/locations/4/comments",
"type" : "application/vnd.example.comment"
}
],
"name" : "foobar",
"id" : "4"
}
],
"_links" : [
{
"rel" : "self",
"href" : "http://localhost:8081/locations",
"type" : "application/vnd.example.location"
}
]
}
<?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.comment"/>
<id><![CDATA[4]]></id>
<name><![CDATA[foobar]]></name>
<created_at><![CDATA[2013-02-07T18:37:04+0100]]></created_at>
</location>
{
"created_at" : "2013-02-07T18:37:04+0100",
"_links" : [
{
"rel" : "self",
"href" : "http://localhost:8081/locations/4",
"type" : "application/vnd.example.location"
},
{
"rel" : "comments",
"href" : "http://localhost:8081/locations/4/comments",
"type" : "application/vnd.example.comment"
}
],
"name" : "foobar",
"id" : "4"
}
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<link href="http://localhost:8081/comments" rel="self" type="application/vnd.example.comment"/>
<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>
</resources>
{
"resources" : [
{
"body" : "",
"created_at" : "2013-02-07T19:41:14+0100",
"_links" : [
{
"rel" : "self",
"href" : "http://localhost:8081/comments/8",
"type" : "application/vnd.example.comment"
}
],
"id" : "8",
"username" : "anonymous"
},
],
"_links" : [
{
"rel" : "self",
"href" : "http://localhost:8081/comments",
"type" : "application/vnd.example.comment"
}
]
}
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.user'
),
),
),
// 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.user'
),
array(
'route' => 'acme_demo.user_all',
'parameters' => array('page' => 'firstPage'),
'rel' => Link::REL_FIRST,
'type' => 'application/vnd.acme.user'
),
array(
'route' => 'acme_demo.user_all',
'parameters' => array('page' => 'lastPage'),
'rel' => Link::REL_LAST,
'type' => 'application/vnd.acme.user'
),
array(
'route' => 'acme_demo.user_all',
'parameters' => array('page' => 'nextPage'),
'rel' => Link::REL_NEXT,
'type' => 'application/vnd.acme.user'
),
array(
'route' => 'acme_demo.user_all',
'parameters' => array('page' => 'previousPage'),
'rel' => Link::REL_PREVIOUS,
'type' => 'application/vnd.acme.user'
),
),
'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": "xxx@example.org",
"_links": [
{
"href": "http://example.com/users/999",
"rel": "self"
}
]
},
// ...
],
"_links": [
{
"href": "http://example.com/users?page=1",
"rel": "self",
"type": "application/vnd.acme.user"
},
{
"href": "http://example.com/users?page=1",
"rel": "previous",
"type":"application/vnd.acme.user"
},
{
"href": "http://example.com/users?page=2",
"rel": "next",
"type":"application/vnd.acme.user"
},
{
"href": "http://example.com/users?page=1",
"rel": "first",
"type":"application/vnd.acme.user"
},
{
"href": "http://example.com/users?page=100",
"rel": "last",
"type":"application/vnd.acme.user"
}
]
}
Hateoas is released under the MIT License. See the bundled LICENSE file for details.