A PHP library to support implementing representations for HATEOAS REST web services.
- Installation
- Usage
- Reference
- Internals
- Versioning
The recommended way to install Hateoas is through
Composer. Require the willdurand/hateoas
package
by running the following command:
composer require willdurand/hateoas
This will resolve the latest stable version.
Otherwise, install the library and setup the autoloader yourself.
If you want to use annotations for configuration you need
to install the doctrine/annotations
package:
composer require doctrine/annotations
If your app uses PHP 8.1 or higher it is recommended to use native PHP attributes. In this case you don't need to install the Doctrine package.
There is a bundle for that! Install the BazingaHateoasBundle, and enjoy!
Important:
For those who use the
1.0
version, you can jump to this documentation page.For those who use the
2.0
version, you can jump to this documentation page.The following documentation has been written for Hateoas 3.0 and above.
Hateoas leverages the Serializer library to provide a nice way to build HATEOAS REST web services. HATEOAS stands for Hypermedia as the Engine of Application State, and adds hypermedia links to your representations (i.e. your API responses). HATEOAS is about the discoverability of actions on a resource.
For instance, let's say you have a User API which returns a representation of a single user as follow:
{
"user": {
"id": 123,
"first_name": "John",
"last_name": "Doe"
}
}
In order to tell your API consumers how to retrieve the data for this specific
user, you have to add your very first link to this representation, let's
call it self
as it is the URI for this particular user:
{
"user": {
"id": 123,
"first_name": "John",
"last_name": "Doe",
"_links": {
"self": { "href": "http://example.com/api/users/123" }
}
}
}
Let's dig into Hateoas now.
In Hateoas terminology, links are seen as relations added to resources. It is worth mentioning that relations also refer to embedded resources too, but this topic will be covered in the Embedding Resources section.
A link is a relation which is identified by a name
(e.g. self
) and that
has an href
parameter:
Annotation (PHP < 8.1)
use JMS\Serializer\Annotation as Serializer;
use Hateoas\Configuration\Annotation as Hateoas;
/**
* @Serializer\XmlRoot("user")
*
* @Hateoas\Relation("self", href = "expr('/api/users/' ~ object.getId())")
*/
class User
{
/** @Serializer\XmlAttribute */
private $id;
private $firstName;
private $lastName;
public function getId() {}
}
Attribute (PHP 8.1 and greater)
use JMS\Serializer\Annotation as Serializer;
use Hateoas\Configuration\Annotation as Hateoas;
#[Serializer\XmlRoot('user')]
#[Hateoas\Relation('self', href: "expr('/api/users/' ~ object.getId())")]
class User
{
#[Serializer\XmlAttribute]
private $id;
private $firstName;
private $lastName;
public function getId() {}
}
In the example above, we configure a self
relation that is a link because of
the href
parameter. Its value, which may look weird at first glance, will be
extensively covered in The Expression Language
section. This special value is used to generate a URI.
In this section, annotations/attributes are used to configure Hateoas. XML and YAML formats are also supported. If you wish, you can use plain PHP too.
Important: you must configure both the Serializer and Hateoas the same way. E.g. if you use YAML for configuring Serializer, use YAML for configuring Hateoas.
The easiest way to try HATEOAS is with the HateoasBuilder
. The builder has
numerous methods to configure the Hateoas serializer, but we won't dig into
them right now (see The HateoasBuilder).
Everything works fine out of the box:
use Hateoas\HateoasBuilder;
$hateoas = HateoasBuilder::create()->build();
$user = new User(42, 'Adrien', 'Brault');
$json = $hateoas->serialize($user, 'json');
$xml = $hateoas->serialize($user, 'xml');
The $hateoas
object is an instance of JMS\Serializer\SerializerInterface
,
coming from the Serializer library. Hateoas does not come with its own
serializer, it hooks into the JMS Serializer.
By default, Hateoas uses the Hypertext Application
Language (HAL) for JSON
serialization. This specifies the structure of the response (e.g. that
"links" should live under a _links
key):
{
"id": 42,
"first_name": "Adrien",
"last_name": "Brault",
"_links": {
"self": {
"href": "/api/users/42"
}
}
}
For XML, Atom Links are used by default:
<user id="42">
<first_name><![CDATA[Adrien]]></first_name>
<last_name><![CDATA[Brault]]></last_name>
<link rel="self" href="/api/users/42"/>
</user>
It is worth mentioning that these formats are the default ones, not the only available ones. You can use different formats through different serializers, and even add your owns.
Now that you know how to add links, let's see how to add embedded resources.
Sometimes, it's more efficient to embed related resources rather than link to them, as it prevents clients from having to make extra requests to fetch those resources.
An embedded resource is a named relation that contains data, represented
by the embedded
parameter.
Annotation (PHP < 8.1)
use JMS\Serializer\Annotation as Serializer;
use Hateoas\Configuration\Annotation as Hateoas;
/**
* ...
*
* @Hateoas\Relation(
* "manager",
* href = "expr('/api/users/' ~ object.getManager().getId())",
* embedded = "expr(object.getManager())",
* exclusion = @Hateoas\Exclusion(excludeIf = "expr(object.getManager() === null)")
* )
*/
class User
{
...
/** @Serializer\Exclude */
private $manager;
}
Attribute (PHP 8.1 and greater)
use JMS\Serializer\Annotation as Serializer;
use Hateoas\Configuration\Annotation as Hateoas;
#[Hateoas\Relation(
'manager',
href: "expr('/api/users/' ~ object.getManager().getId())",
embedded: "expr(object.getManager())",
exclusion: new Hateoas\Exclusion(excludeif: "expr(object.getManager() === null)"),
)]
class User
{
...
#[Serializer\Exclude]
private $manager;
}
Note: You will need to exclude the manager property from the serialization,
otherwise both the serializer and Hateoas will serialize it.
You will also have to exclude the manager relation when the manager is null
,
because otherwise an error will occur when creating the href
link (calling
getId()
on null
).
Tip: If the manager property is an object that already has a _self
link, you can re-use that value for the href
instead of repeating it here.
See LinkHelper.
$hateoas = HateoasBuilder::create()->build();
$user = new User(42, 'Adrien', 'Brault', new User(23, 'Will', 'Durand'));
$json = $hateoas->serialize($user, 'json');
$xml = $hateoas->serialize($user, 'xml');
For json
, the HAL representation places these embedded relations inside
an _embedded
key:
{
"id": 42,
"first_name": "Adrien",
"last_name": "Brault",
"_links": {
"self": {
"href": "/api/users/42"
},
"manager": {
"href": "/api/users/23"
}
},
"_embedded": {
"manager": {
"id": 23,
"first_name": "Will",
"last_name": "Durand",
"_links": {
"self": {
"href": "/api/users/23"
}
}
}
}
}
In XML, serializing embedded
relations will create new elements:
<user id="42">
<first_name><![CDATA[Adrien]]></first_name>
<last_name><![CDATA[Brault]]></last_name>
<link rel="self" href="/api/users/42"/>
<link rel="manager" href="/api/users/23"/>
<manager rel="manager" id="23">
<first_name><![CDATA[Will]]></first_name>
<last_name><![CDATA[Durand]]></last_name>
<link rel="self" href="/api/users/23"/>
</manager>
</user>
The tag name of an embedded resource is inferred from the
@XmlRoot
annotation (xml_root_name
in YAML, xml-root-name
in XML) coming from the
Serializer configuration.
The library provides several classes in the Hateoas\Representation\*
namespace to help you with common tasks. These are simple classes configured
with the library's annotations.
The PaginatedRepresentation
, OffsetRepresentation
and CollectionRepresentation
classes are
probably the most interesting ones. These are helpful when your resource is
actually a collection of resources (e.g. /users
is a collection of users).
These help you represent the collection and add pagination and limits:
use Hateoas\Representation\PaginatedRepresentation;
use Hateoas\Representation\CollectionRepresentation;
$paginatedCollection = new PaginatedRepresentation(
new CollectionRepresentation(array($user1, $user2, ...)),
'user_list', // route
array(), // route parameters
1, // page number
20, // limit
4, // total pages
'page', // page route parameter name, optional, defaults to 'page'
'limit', // limit route parameter name, optional, defaults to 'limit'
false, // generate relative URIs, optional, defaults to `false`
75 // total collection size, optional, defaults to `null`
);
$json = $hateoas->serialize($paginatedCollection, 'json');
$xml = $hateoas->serialize($paginatedCollection, 'xml');
The CollectionRepresentation
offers a basic representation of an embedded collection.
The PaginatedRepresentation
is designed to add self
, first
, and when
possible last
, next
, and previous
links.
The OffsetRepresentation
works just like PaginatedRepresentation
but is useful
when pagination is expressed by offset
, limit
and total
.
The RouteAwareRepresentation
adds a self
relation based on a given route.
You can generate absolute URIs by setting the absolute
parameter to true
in both the PaginatedRepresentation
and the RouteAwareRepresentation
.
The Hateoas library also provides a PagerfantaFactory
to easily build
PaginatedRepresentation
from a
Pagerfanta instance. If you use
the Pagerfanta library, this is an easier way to create the collection
representations:
use Hateoas\Configuration\Route;
use Hateoas\Representation\Factory\PagerfantaFactory;
$pagerfantaFactory = new PagerfantaFactory(); // you can pass the page,
// and limit parameters name
$paginatedCollection = $pagerfantaFactory->createRepresentation(
$pager,
new Route('user_list', array())
);
$json = $hateoas->serialize($paginatedCollection, 'json');
$xml = $hateoas->serialize($paginatedCollection, 'xml');
You would get the following JSON content:
{
"page": 1,
"limit": 10,
"pages": 1,
"_links": {
"self": {
"href": "/api/users?page=1&limit=10"
},
"first": {
"href": "/api/users?page=1&limit=10"
},
"last": {
"href": "/api/users?page=1&limit=10"
}
},
"_embedded": {
"items": [
{ "id": 123 },
{ "id": 456 }
]
}
}
And the following XML content:
<?xml version="1.0" encoding="UTF-8"?>
<collection page="1" limit="10" pages="1">
<entry id="123"></entry>
<entry id="456"></entry>
<link rel="self" href="/api/users?page=1&limit=10" />
<link rel="first" href="/api/users?page=1&limit=10" />
<link rel="last" href="/api/users?page=1&limit=10" />
</collection>
If you want to customize the inlined CollectionRepresentation
, pass one as
third argument of the createRepresentation()
method:
use Hateoas\Representation\Factory\PagerfantaFactory;
$pagerfantaFactory = new PagerfantaFactory(); // you can pass the page and limit parameters name
$paginatedCollection = $pagerfantaFactory->createRepresentation(
$pager,
new Route('user_list', array()),
new CollectionRepresentation($pager->getCurrentPageResults())
);
$json = $hateoas->serialize($paginatedCollection, 'json');
$xml = $hateoas->serialize($paginatedCollection, 'xml');
If you want to change the xml root name of the collection, create a new class with the xml root configured and use the inline mechanism:
Annotation (PHP < 8.1)
use JMS\Serializer\Annotation as Serializer;
/**
* @Serializer\XmlRoot("users")
*/
class UsersRepresentation
{
/**
* @Serializer\Inline
*/
private $inline;
public function __construct($inline)
{
$this->inline = $inline;
}
}
$paginatedCollection = ...;
$paginatedCollection = new UsersRepresentation($paginatedCollection);
Attribute (PHP 8.1 and greater)
use JMS\Serializer\Annotation as Serializer;
#[Serializer\XmlRoot('users')]
class UsersRepresentation
{
#[Serializer\Inline]
private $inline;
public function __construct($inline)
{
$this->inline = $inline;
}
}
$paginatedCollection = ...;
$paginatedCollection = new UsersRepresentation($paginatedCollection);
As mentionned in the previous section, representations are classes configured with the library's annotations in order to help you with common tasks. The collection representations are described in Dealing With Collection.
The VndErrorRepresentation
allows you to describe an error response following
the vnd.error
specification.
$error = new VndErrorRepresentation(
'Validation failed',
42,
'http://.../',
'http://.../'
);
Serializing such a representation in XML and JSON would give you the following outputs:
<?xml version="1.0" encoding="UTF-8"?>
<resource logref="42">
<message><![CDATA[Validation failed]]></message>
<link rel="help" href="http://.../"/>
<link rel="describes" href="http://.../"/>
</resource>
{
"message": "Validation failed",
"logref": 42,
"_links": {
"help": {
"href": "http://.../"
},
"describes": {
"href": "http://.../"
}
}
}
Hint: it is recommended to create your own error classes that extend the
VndErrorRepresentation
class.
Hateoas relies on the powerful Symfony ExpressionLanguage component to retrieve values such as links, ids or objects to embed.
Each time you fill in a value (e.g. a Relation href
in annotations or YAML),
you can either pass a hardcoded value or an expression.
In order to use the Expression Language, you have to use the expr()
notation:
Annotation (PHP < 8.1)
use Hateoas\Configuration\Annotation as Hateoas;
/**
* @Hateoas\Relation("self", href = "expr('/api/users/' ~ object.getId())")
*/
Attribute (PHP 8.1 and greater)
use Hateoas\Configuration\Annotation as Hateoas;
#[Hateoas\Relation('self', href: "expr('/api/users/' ~ object.getId())")]
You can learn more about the Expression Syntax by reading the official documentation: The Expression Syntax.
Natively, a special variable named object
is available in each expression, and
represents the current object:
expr(object.getId())
We call such a variable a context variable.
You can add your own context variables to the Expression Language context by adding them to the expression evaluator.
Using the HateoasBuilder
, call the setExpressionContextVariable()
method to add
new context variables:
use Hateoas\HateoasBuilder;
$hateoas = HateoasBuilder::create()
->setExpressionContextVariable('foo', new Foo())
->build();
The foo
variable is now available:
expr(foo !== null)
For more info on how to add functions to the expression language, please refer to https://symfony.com/doc/current/components/expression_language/extending.html
Since you can use the Expression Language to define
the relations links (href
key), you can do a lot by default. However if you
are using a framework, chances are that you will want to use routes to build
links.
You will first need to configure an UrlGenerator
on the builder. You can
either implement the Hateoas\UrlGenerator\UrlGeneratorInterface
, or use the
Hateoas\UrlGenerator\CallableUrlGenerator
:
use Hateoas\UrlGenerator\CallableUrlGenerator;
$hateoas = HateoasBuilder::create()
->setUrlGenerator(
null, // By default all links uses the generator configured with the null name
new CallableUrlGenerator(function ($route, array $parameters, $absolute) use ($myFramework) {
return $myFramework->generateTheUrl($route, $parameters, $absolute);
})
)
->build()
;
You will then be able to use the @Route annotation:
Annotation (PHP < 8.1)
use Hateoas\Configuration\Annotation as Hateoas;
/**
* @Hateoas\Relation(
* "self",
* href = @Hateoas\Route(
* "user_get",
* parameters = {
* "id" = "expr(object.getId())"
* }
* )
* )
*/
class User
Attribute (PHP 8.1 and greater)
use Hateoas\Configuration\Annotation as Hateoas;
#[Hateoas\Relation(
'self',
href: new Hateoas\Route(
'user_get',
parameters: [
'id' => 'expr(object.getId())',
],
)
)]
class User
{
"id": 42,
"first_name": "Adrien",
"last_name": "Brault",
"_links": {
"self": {
"href": "/api/users/42"
}
}
}
Note that the library comes with a SymfonyUrlGenerator
. For example, to use it
in Silex:
use Hateoas\UrlGenerator\SymfonyUrlGenerator;
$hateoas = HateoasBuilder::create()
->setUrlGenerator(null, new SymfonyUrlGenerator($app['url_generator']))
->build()
;
Hateoas provides a set of helpers to ease the process of building APIs.
The LinkHelper
class provides a getLinkHref($object, $rel, $absolute = false)
method that allows you to get the href value of any object, for any given
relation name. It is able to generate a URI (either absolute or relative) from
any link relation:
$user = new User(123, 'William', 'Durand');
$linkHelper->getLinkHref($user, 'self');
// /api/users/123
$linkHelper->getLinkHref($user, 'self', true);
// http://example.com/api/users/123
The feature above is also available in your expressions (cf. The Expression
Language) through the link(object, rel, absolute)
function:
Annotation (PHP < 8.1)
/**
* @Hateoas\Relation(
* "self",
* href = @Hateoas\Route("post_get", parameters = {"id" = "expr(object.getId())"})
* )
*/
class Post {}
/**
* @Hateoas\Relation(
* "self",
* href = @Hateoas\Route("user_get", parameters = {"id" = "expr(object.getId())"})
* )
* @Hateoas\Relation(
* "post",
* href = "expr(link(object.getPost(), 'self', true))"
* )
* @Hateoas\Relation(
* "relative",
* href = "expr(link(object.getRelativePost(), 'self'))"
* )
*/
class User
{
...
public function getPost()
{
return new Post(456);
}
public function getRelativePost()
{
return new Post(789);
}
}
Attribute (PHP 8.1 and greater)
#[Hateoas\Relation(
'self',
href: new Hateoas\Route(
'post_get',
parameters: [
'id' => 'expr(object.getId())',
],
),
)]
class Post {}
#[Hateoas\Relation(
'self',
href: new Hateoas\Route(
'user_get',
parameters: [
'id' => 'expr(object.getId())',
],
),
)]
#[Hateoas\Relation(
'post',
href: "expr(link(object.getPost(), 'self', true))",
)]
#[Hateoas\Relation(
'relative',
href: "expr(link(object.getRelativePost(), 'self'))",
)]
class User
{
...
public function getPost()
{
return new Post(456);
}
public function getRelativePost()
{
return new Post(789);
}
}
Pay attention to the href
expressions for the post
and relative
relations,
as well as their corresponding values in the following JSON content:
{
"user": {
"id": 123,
"first_name": "William",
"last_name": "Durand",
"_links": {
"self": { "href": "http://example.com/api/users/123" },
"post": { "href": "http://example.com/api/posts/456" },
"relative": { "href": "/api/posts/789" }
}
}
}
It is worth mentioning that you can force whether you want an absolute or
relative URI by using the third argument in both the getLinkHref()
method and
the link
function.
Important: by default, all URIs will be relative, even those which are defined as absolute in their configuration.
$linkHelper->getLinkHref($user, 'post');
// /api/posts/456
$linkHelper->getLinkHref($user, 'post', true);
// http://example.com/api/posts/456
$linkHelper->getLinkHref($user, 'relative');
// /api/posts/789
$linkHelper->getLinkHref($user, 'relative', true);
// http://example.com/api/posts/789
Hateoas also provides a set of Twig extensions.
The LinkExtension
allows you to use the LinkHelper into your
Twig templates, so that you can generate links in your HTML templates for
instance.
This extension exposes the getLinkHref()
helper's method through the
link_href
Twig function:
{{ link_href(user, 'self') }}
{# will generate: /users/123 #}
{{ link_href(will, 'self', false) }}
{# will generate: /users/123 #}
{{ link_href(will, 'self', true) }}
{# will generate: http://example.com/users/123 #}
Hateoas provides a set of serializers. Each serializer allows you to generate either XML or JSON content following a specific format, such as HAL, or Atom Links for instance.
The JsonHalSerializer
allows you to generate HAL compliant relations in JSON.
It is the default JSON serializer in Hateoas.
HAL provides its linking capability with a convention which says that a resource
object has a reserved property called _links
. This property is an object that
contains links. These links are key'ed by their link relation.
HAL also describes another convention which says that a resource may have
another reserved property named _embedded
. This property is similar to _links
in that embedded resources are key'ed by relation name. The main difference is
that rather than being links, the values are resource objects.
{
"message": "Hello, World!",
"_links": {
"self": {
"href": "/notes/0"
}
},
"_embedded": {
"associated_events": [
{
"name": "SymfonyCon",
"date": "2013-12-12T00:00:00+0100"
}
]
}
}
The XmlSerializer
allows you to generate Atom
Links into your XML
documents. It is the default XML serializer.
<?xml version="1.0" encoding="UTF-8"?>
<note>
<message><![CDATA[Hello, World!]]></message>
<link rel="self" href="/notes/0" />
<events rel="associated_events">
<event>
<name><![CDATA[SymfonyCon]]></name>
<date><![CDATA[2013-12-12T00:00:00+0100]]></date>
</event>
</events>
</note>
The XmlHalSerializer
allows you to generate HAL compliant relations in XML.
HAL in XML is similar to HAL in JSON in the sense that
it describes link
tags and resource
tags.
Note: the self
relation will actually become an attribute of the main
resource instead of being a link
tag. Other links will be generated as link
tags.
<?xml version="1.0" encoding="UTF-8"?>
<note href="/notes/0">
<message><![CDATA[Hello, World!]]></message>
<resource rel="associated_events">
<name><![CDATA[SymfonyCon]]></name>
<date><![CDATA[2013-12-12T00:00:00+0100]]></date>
</resource>
</note>
You must implement the SerializerInterface
that describes two methods to serialize
links and embedded relations.
The HateoasBuilder
class is used to easily configure Hateoas thanks to a
powerful and fluent API.
use Hateoas\HateoasBuilder;
$hateoas = HateoasBuilder::create()
->setCacheDir('/path/to/cache/dir')
->setDebug($trueOrFalse)
->setDefaultXmlSerializer()
...
->build();
All the methods below return the current builder, so that you can chain them.
setXmlSerializer(SerializerInterface $xmlSerializer)
: sets the XML serializer to use. Default is:XmlSerializer
;setDefaultXmlSerializer()
: sets the default XML serializer (XmlSerializer
).
setJsonSerializer(SerializerInterface $jsonSerializer)
: sets the JSON serializer to use. Default is:JsonHalSerializer
;setDefaultJsonSerializer()
: sets the default JSON serializer (JsonHalSerializer
).
setUrlGenerator($name = null, UrlGeneratorInterface $urlGenerator)
: adds a new named URL generator. If$name
isnull
, the URL generator will be the default one.
setExpressionContextVariable($name, $value)
: adds a new expression context variable;setExpressionLanguage(ExpressionLanguage $expressionLanguage)
;
includeInterfaceMetadata($include)
: whether to include the metadata from the interfaces;setMetadataDirs(array $namespacePrefixToDirMap)
: sets a map of namespace prefixes to directories. This method overrides any previously defined directories;addMetadataDir($dir, $namespacePrefix = '')
: adds a directory where the serializer will look for class metadata;addMetadataDirs(array $namespacePrefixToDirMap)
: adds a map of namespace prefixes to directories;replaceMetadataDir($dir, $namespacePrefix = '')
: similar toaddMetadataDir()
, but overrides an existing entry.
Please read the official Serializer documentation for more details.
setDebug($debug)
: enables or disables the debug mode;setCacheDir($dir)
: sets the cache directory.
Both the serializer and the Hateoas libraries collect metadata about your objects from various sources such as YML, XML, or annotations. In order to make this process as efficient as possible, it is recommended that you allow the Hateoas library to cache this information. To do that, configure a cache directory:
$builder = \Hateoas\HateoasBuilder::create();
$hateoas = $builder
->setCacheDir($someWritableDir)
->build();
Hateoas supports several metadata sources. By default, it uses Doctrine annotations (PHP < 8.1) or native PHP attributes (PHP >= 8.1), but you may also store metadata in XML, or YAML files. For the latter, it is necessary to configure a metadata directory where those files are located:
$hateoas = \Hateoas\HateoasBuilder::create()
->addMetadataDir($someDir)
->build();
Hateoas would expect the metadata files to be named like the fully qualified
class names where all \
are replaced with .
. If you class would be named
Vendor\Package\Foo
the metadata file would need to be located at
$someDir/Vendor.Package.Foo.(xml|yml)
.
Hateoas allows frameworks to dynamically add relations to classes by providing an extension point at configuration level. This feature can be useful for those who want to to create a new layer on top of Hateoas, or to add "global" relations rather than copying the same configuration on each class.
In order to leverage this mechanism, the ConfigurationExtensionInterface
interface has to be implemented:
use Hateoas\Configuration\Metadata\ConfigurationExtensionInterface;
use Hateoas\Configuration\Metadata\ClassMetadataInterface;
use Hateoas\Configuration\Relation;
class AcmeFooConfigurationExtension implements ConfigurationExtensionInterface
{
/**
* {@inheritDoc}
*/
public function decorate(ClassMetadataInterface $classMetadata): void
{
if (0 === strpos('Acme\Foo\Model', $classMetadata->getName())) {
// Add a "root" relation to all classes in the `Acme\Foo\Model` namespace
$classMetadata->addRelation(
new Relation(
'root',
'/'
)
);
}
}
}
You can access the existing relations loaded from Annotations, XML, or YAML with
$classMetadata->getRelations()
.
If the $classMetadata
has relations, or if you add relations to it, its
relations will be cached. So if you read configuration files (Annotations, XML,
or YAML), make sure to reference them on the class metadata:
$classMetadata->fileResources[] = $file;
<?xml version="1.0" encoding="UTF-8"?>
<serializer>
<class name="Acme\Demo\Representation\User" h:providers="Class::getRelations expr(sevice('foo').getMyAdditionalRelations())" xmlns:h="https://github.com/willdurand/Hateoas">
<h:relation rel="self">
<h:href uri="http://acme.com/foo/1" />
</h:relation>
<h:relation rel="friends">
<h:href route="user_friends" generator="my_custom_generator">
<h:parameter name="id" value="expr(object.getId())" />
<h:parameter name="page" value="1" />
</h:ref>
<h:embedded xml-element-name="users">
<h:content>expr(object.getFriends())</h:content>
<h:exclusion ... />
</h:embedded>
<h:exclusion groups="Default, user_full" since-version="1.0" until-version="2.2" exclude-if="expr(object.getFriends() === null)" />
</h:relation>
</class>
</serializer>
See the
hateoas.xsd
file for more details.
Acme\Demo\Representation\User:
relations:
-
rel: self
href: http://acme.com/foo/1
-
rel: friends
href:
route: user_friends
parameters:
id: expr(object.getId())
page: 1
generator: my_custom_generator
absolute: false
embedded:
content: expr(object.getFriends())
xmlElementName: users
exclusion: ...
exclusion:
groups: [Default, user_full]
since_version: 1.0
until_version: 2.2
exclude_if: expr(object.getFriends() === null)
relation_providers: [ "Class::getRelations", "expr(sevice('foo').getMyAdditionalRelations())" ]
This annotation can be defined on a class.
Annotation (PHP < 8.1)
use Hateoas\Configuration\Annotation as Hateoas;
/**
* @Hateoas\Relation(
* name = "self",
* href = "http://hello",
* embedded = "expr(object.getHello())",
* attributes = { "foo" = "bar" },
* exclusion = ...,
* )
*/
Attribute (PHP 8.1 and greater)
use Hateoas\Configuration\Annotation as Hateoas;
#[Hateoas\Relation(
name: 'self',
href: 'http://hello',
embedded: 'expr(object.getHello())',
attributes: ['foo' => 'bar'],
exclusion: '...',
)]
Property | Required | Content | Expression language |
---|---|---|---|
name | Yes | string | No |
href | If embedded is not set | string / @Route | Yes |
embedded | If href is not set | string / @Embedded | Yes |
attributes | No | array | Yes on values |
exclusion | No | @Exclusion | N/A |
Important: attributes
are only used on link relations (i.e. combined
with the href
property, not with the embedded
one).
Annotation (PHP < 8.1)
use Hateoas\Configuration\Annotation as Hateoas;
/**
* @Hateoas\Relation(
* name = "self",
* href = @Hateoas\Route(
* "user_get",
* parameters = { "id" = "expr(object.getId())" },
* absolute = true,
* generator = "my_custom_generator"
* )
* )
*/
Attribute (PHP 8.1 and greater)
use Hateoas\Configuration\Annotation as Hateoas;
#[Hateoas\Relation(
name: 'self',
href: new Hateoas\Route(
'user_get',
parameters: ['id' = 'expr(object.getId())'],
absolute: true,
generator: 'my_custom_generator',
),
)]
This annotation can be defined in the href property of the @Relation annotation. This is allows you to your URL generator, if you have configured one.
Property | Required | Content | Expression language |
---|---|---|---|
name | Yes | string | No |
parameters | Defaults to array() | array / string | Yes (string + array values) |
absolute | Defaults to false | boolean / string | Yes |
generator | No | string / null | No |
Annotation (PHP < 8.1)
use Hateoas\Configuration\Annotation as Hateoas;
/**
* @Hateoas\Relation(
* name = "friends",
* embedded = @Hateoas\Embedded(
* "expr(object.getFriends())",
* exclusion = ...,
* xmlElementName = "users"
* )
* )
*/
Attribute (PHP 8.1 and greater)
use Hateoas\Configuration\Annotation as Hateoas;
#[Hateoas\Relation(
name: 'friends',
embedded: new Hateoas\Embedded(
'expr(object.getFriends())',
exclusion: '...',
xmlElementName: 'users',
),
)]
This annotation can be defined in the embedded property of the
@Relation annotation. It is useful if you need configure the
exclusion
or xmlElementName
options for the embedded resource.
Property | Required | Content | Expression language |
---|---|---|---|
content | Yes | string / array | Yes (string) |
exclusion | Defaults to array() | @Exclusion | N/A |
xmlElementName | Defaults to array() | string | No |
This annotation can be defined in the exclusion property of both the @Relation and @Embedded annotations.
Property | Required | Content | Expression language |
---|---|---|---|
groups | No | array | No |
sinceVersion | No | string | No |
untilVersion | No | string | No |
maxDepth | No | integer | No |
excludeIf | No | string / boolean | Yes |
All values except excludeIf
act the same way as when they are used directly
on the regular properties with the serializer.
excludeIf
expects a boolean and is helpful when another expression would fail
under some circumstances. In this example, if the getManager
method is null
,
you should exclude it to prevent the URL generation from failing:
Annotation (PHP < 8.1)
use Hateoas\Configuration\Annotation as Hateoas;
/**
* @Hateoas\Relation(
* "manager",
* href = @Hateoas\Route(
* "user_get",
* parameters = { "id" = "expr(object.getManager().getId())" }
* ),
* exclusion = @Hateoas\Exclusion(excludeIf = "expr(null === object.getManager())")
* )
*/
class User
{
public function getId() {}
/**
* @return User|null
*/
public function getManager() {}
}
Attribute (PHP 8.1 and greater)
use Hateoas\Configuration\Annotation as Hateoas;
#[Hateoas\Relation(
name: 'manager',
href: new Hateoas\Route(
'user_get',
parameters: ['id' => 'expr(object.getManager().getId())'],
),
exclusion: new Hateoas\Exclusion(excludeIf: 'expr(null === object.getManager())')
)]
class User
{
public function getId() {}
public function getManager(): ?User {}
}
This annotation can be defined on a class. It is useful if you wish to serialize multiple-relations(links). As an example:
{
"_links": {
"relation_name": [
{"href": "link1"},
{"href": "link2"},
{"href": "link3"}
]
}
}
Property | Required | Content | Expression language |
---|---|---|---|
name | Yes | string | Yes |
It can be "name":
- A function:
my_func
- A static method:
MyClass::getExtraRelations
- An expression:
expr(service('user.rel_provider').getExtraRelations())
Here and example using the expression language:
Annotation (PHP < 8.1)
use Hateoas\Configuration\Annotation as Hateoas;
/**
* @Hateoas\RelationProvider("expr(service('user.rel_provider').getExtraRelations())")
*/
class User
{
...
}
Attribute (PHP 8.1 and greater)
use Hateoas\Configuration\Annotation as Hateoas;
#[Hateoas\RelationProvider("expr(service('user.rel_provider').getExtraRelations())")]
class User
{
...
}
Here the UserRelPrvider
class:
use Hateoas\Configuration\Relation;
use Hateoas\Configuration\Route;
class UserRelPrvider
{
private $evaluator;
public function __construct(CompilableExpressionEvaluatorInterface $evaluator)
{
$this->evaluator = $evaluator;
}
/**
* @return Relation[]
*/
public function getExtraRelations(): array
{
// You need to return the relations
return array(
new Relation(
'self',
new Route(
'foo_get',
['id' => $this->evaluator->parse('object.getId()', ['object'])]
)
)
);
}
}
$this->evaluator
implementing CompilableExpressionEvaluatorInterface
is used to parse the expression language
in a form that can be cached and saved for later use.
If you do not need the expression language in your relations, then this service is not needed.
The user.rel_provider
service is defined as:
user.rel_provider:
class: UserRelPrvider
arguments:
- '@jms_serializer.expression_evaluator'
In this case jms_serializer.expression_evaluator
is a service implementing CompilableExpressionEvaluatorInterface
.
This section refers to the Hateoas internals, providing documentation about hidden parts of this library. This is not always relevant for end users, but interesting for developers or people interested in learning how things work under the hood.
willdurand/hateoas
follows Semantic Versioning.
As of October 2013, versions 1.x
and 0.x
are officially not supported anymore
(note that 1.x
was never released).
Version 3.x
is the current major stable version.
Version 2.x
is maintained only for security bug fixes and for major issues that might occur.
See CONTRIBUTING file.
Install the Composer dev
dependencies:
php composer.phar install --dev
Then, run the test suite using PHPUnit:
bin/phpunit
Hateoas is released under the MIT License. See the bundled LICENSE file for details.