PHPackages                             willdurand/hateoas - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. willdurand/hateoas

ActiveLibrary[API Development](/categories/api)

willdurand/hateoas
==================

A PHP library to support implementing representations for HATEOAS REST web services

3.14.0(4mo ago)1.0k15.8M↓13.3%120[31 issues](https://github.com/willdurand/Hateoas/issues)[2 PRs](https://github.com/willdurand/Hateoas/pulls)19MITPHPPHP ^8.1CI passing

Since Feb 8Pushed 4mo ago35 watchersCompare

[ Source](https://github.com/willdurand/Hateoas)[ Packagist](https://packagist.org/packages/willdurand/hateoas)[ RSS](/packages/willdurand-hateoas/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (10)Dependencies (14)Versions (58)Used By (19)

Hateoas
=======

[](#hateoas)

[![GitHub Actions](https://github.com/willdurand/hateoas/workflows/CI/badge.svg)](https://github.com/willdurand/hateoas/actions?query=workflow%3A%22CI%22+branch%3Amaster)[![GitHub Actions](https://github.com/willdurand/hateoas/workflows/Coding%20Standards/badge.svg)](https://github.com/willdurand/hateoas/actions?query=workflow%3A%22Coding%20Standards%22+branch%3Amaster)[![Latest Stable Version](https://camo.githubusercontent.com/421b034d2d04b832b99db3d924134d847335018d47b1f44ce77c5a92a67aed8a/68747470733a2f2f706f7365722e707567782e6f72672f77696c6c647572616e642f686174656f61732f762f737461626c652e706e67)](https://packagist.org/packages/willdurand/hateoas)[![PHP Version Require](https://camo.githubusercontent.com/4eacf2d1a226faf588e9b8e1cdafd3add70e5dfe70160a5f5524d94807149055/68747470733a2f2f706f7365722e707567782e6f72672f77696c6c647572616e642f686174656f61732f726571756972652f706870)](https://packagist.org/packages/willdurand/hateoas)

A PHP library to support implementing representations for HATEOAS REST web services.

- [Installation](#installation)
    - [Working With Symfony](#working-with-symfony)
- [Usage](#usage)
    - [Introduction](#introduction)
    - [Configuring Links](#configuring-links)
    - [Embedding Resources](#embedding-resources)
    - [Dealing With Collections](#dealing-with-collections)
    - [Representations](#representations)
        - [VndErrorRepresentation](#vnderrorrepresentation)
    - [The Expression Language](#the-expression-language)
        - [Context](#context)
        - [Adding Your Own Context Variables](#adding-your-own-context-variables)
        - [Expression Functions](#expression-functions)
    - [URL Generators](#url-generators)
    - [Helpers](#helpers)
        - [LinkHelper](#linkhelper)
    - [Twig Extensions](#twig-extensions)
        - [LinkExtension](#linkextension)
    - [Serializers &amp; Formats](#serializers--formats)
        - [The JsonHalSerializer](#the-jsonhalserializer)
        - [The XmlSerializer](#the-xmlserializer)
        - [The XmlHalSerializer](#the-xmlhalserializer)
        - [Adding New Serializers](#adding-new-serializers)
    - [The HateoasBuilder](#the-hateoasbuilder)
        - [XML Serializer](#xml-serializer)
        - [JSON Serializer](#json-serializer)
        - [URL Generator](#url-generator)
        - [Expression Evaluator/Expression Language](#expression-evaluatorexpression-language)
        - [Relation Provider](#relationprovider)
        - [(JMS) Serializer Specific](#jms-serializer-specific)
        - [Others](#others)
    - [Configuring a Cache Directory](#configuring-a-cache-directory)
    - [Configuring Metadata Locations](#configuring-metadata-locations)
    - [Extending The Library](#extending-the-library)
- [Reference](#reference)
    - [XML](#xml)
    - [YAML](#yaml)
    - [Annotations/Attributes](#annotations)
        - [@Relation](#relation)
        - [@Route](#route)
        - [@Embedded](#embedded)
        - [@Exclusion](#exclusion)
        - [@RelationProvider](#relationprovider)
- [Internals](#internals)
- [Versioning](#versioning)

Installation
------------

[](#installation)

The recommended way to install Hateoas is through [Composer](http://getcomposer.org/). 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**](#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.

### Working With Symfony

[](#working-with-symfony)

There is a bundle for that! Install the [BazingaHateoasBundle](https://github.com/willdurand/BazingaHateoasBundle), and enjoy!

Usage
-----

[](#usage)

> **Important:**
>
> For those who use the `1.0` version, you can [jump to this documentation page](https://github.com/willdurand/Hateoas/blob/1.0/README.md#readme).
>
> For those who use the `2.0` version, you can [jump to this documentation page](https://github.com/willdurand/Hateoas/blob/2.0/README.md#readme).
>
> The following documentation has been written for **Hateoas 3.0** and above.

### Introduction

[](#introduction)

**Hateoas** leverages the [Serializer](https://github.com/schmittjoh/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](http://timelessrepo.com/haters-gonna-hateoas).

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.

### Configuring Links

[](#configuring-links)

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](#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 &lt; 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](#the-expression-language)section. This special value is used to generate a URI.

In this section, [**annotations/attributes**](#annotations) are used to configure Hateoas. [**XML**](#xml) and [**YAML**](#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](#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](http://stateless.co/hal_specification.html) (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](http://tools.ietf.org/search/rfc4287#section-4.2.7)are used by default:

```

    Adrien]]>
    Brault]]>

```

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](#serializers--formats).

Now that you know how to add **links**, let's see how to add **embedded resources**.

### Embedding Resources

[](#embedding-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 &lt; 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](#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:

```

    Adrien]]>
    Brault]]>

        Will]]>
        Durand]]>

```

The tag name of an embedded resource is inferred from the [`@XmlRoot`](http://jmsyst.com/libs/serializer/master/reference/annotations#xmlroot)annotation (`xml_root_name` in YAML, `xml-root-name` in XML) coming from the Serializer configuration.

### Dealing With Collections

[](#dealing-with-collections)

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](https://github.com/BabDev/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:

```

```

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 &lt; 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);
```

### Representations

[](#representations)

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](#dealing-with-collections).

#### VndErrorRepresentation

[](#vnderrorrepresentation)

The `VndErrorRepresentation` allows you to describe an error response following the [`vnd.error` specification](https://github.com/blongden/vnd.error).

```
$error = new VndErrorRepresentation(
    'Validation failed',
    42,
    'http://.../',
    'http://.../'
);
```

Serializing such a representation in XML and JSON would give you the following outputs:

```

    Validation failed]]>

```

```
{
    "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.

### The Expression Language

[](#the-expression-language)

Hateoas relies on the powerful Symfony [ExpressionLanguage](http://symfony.com/doc/current/components/expression_language/introduction.html)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 &lt; 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](http://symfony.com/doc/current/components/expression_language/syntax.html).

#### Context

[](#context)

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.

##### Adding Your Own Context Variables

[](#adding-your-own-context-variables)

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)

```

##### Expression Functions

[](#expression-functions)

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](https://symfony.com/doc/current/components/expression_language/extending.html)

### URL Generators

[](#url-generators)

Since you can use the [Expression Language](#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](#route) annotation:

Annotation (PHP &lt; 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()
;
```

### Helpers

[](#helpers)

Hateoas provides a set of helpers to ease the process of building APIs.

#### LinkHelper

[](#linkhelper)

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 `link` Function

[](#the-link-function)

The feature above is also available in your expressions (cf. [The Expression Language](#the-expression-language)) through the `link(object, rel, absolute)`**function**:

Annotation (PHP &lt; 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
```

### Twig Extensions

[](#twig-extensions)

Hateoas also provides a set of [Twig](http://twig.sensiolabs.org) extensions.

#### LinkExtension

[](#linkextension)

The `LinkExtension` allows you to use the [LinkHelper](#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 #}
```

### Serializers &amp; Formats

[](#serializers--formats)

Hateoas provides a set of **serializers**. Each **serializer** allows you to generate either XML or JSON content following a specific **format**, such as [HAL](http://stateless.co/hal_specification.html), or [Atom Links](http://tools.ietf.org/search/rfc4287#section-4.2.7) for instance.

#### The JsonHalSerializer

[](#the-jsonhalserializer)

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.

[![](https://camo.githubusercontent.com/d1fa10da0307c60cfe0bfeba2fe759a70bdbf40b273f902fb91c6daac6de4b3d/687474703a2f2f73746174656c6573732e636f2f696e666f2d6d6f64656c2e706e67)](https://camo.githubusercontent.com/d1fa10da0307c60cfe0bfeba2fe759a70bdbf40b273f902fb91c6daac6de4b3d/687474703a2f2f73746174656c6573732e636f2f696e666f2d6d6f64656c2e706e67)

```
{
    "message": "Hello, World!",
    "_links": {
        "self": {
            "href": "/notes/0"
        }
    },
    "_embedded": {
        "associated_events": [
            {
                "name": "SymfonyCon",
                "date": "2013-12-12T00:00:00+0100"
            }
        ]
    }
}
```

#### The XmlSerializer

[](#the-xmlserializer)

The `XmlSerializer` allows you to generate [Atom Links](http://tools.ietf.org/search/rfc4287#section-4.2.7) into your XML documents. It is the default XML serializer.

```

    Hello, World!]]>

            SymfonyCon]]>
            2013-12-12T00:00:00+0100]]>

```

#### The XmlHalSerializer

[](#the-xmlhalserializer)

The `XmlHalSerializer` allows you to generate HAL compliant relations in XML.

HAL in XML is similar to [HAL in JSON](#the-jsonhalserializer) 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.

```

    Hello, World!]]>

        SymfonyCon]]>
        2013-12-12T00:00:00+0100]]>

```

#### Adding New Serializers

[](#adding-new-serializers)

You must implement the `SerializerInterface` that describes two methods to serialize **links** and **embedded** relations.

### The HateoasBuilder

[](#the-hateoasbuilder)

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.

#### XML Serializer

[](#xml-serializer)

- `setXmlSerializer(SerializerInterface $xmlSerializer)`: sets the XML serializer to use. Default is: `XmlSerializer`;
- `setDefaultXmlSerializer()`: sets the default XML serializer (`XmlSerializer`).

#### JSON Serializer

[](#json-serializer)

- `setJsonSerializer(SerializerInterface $jsonSerializer)`: sets the JSON serializer to use. Default is: `JsonHalSerializer`;
- `setDefaultJsonSerializer()`: sets the default JSON serializer (`JsonHalSerializer`).

#### URL Generator

[](#url-generator)

- `setUrlGenerator($name = null, UrlGeneratorInterface $urlGenerator)`: adds a new named URL generator. If `$name` is `null`, the URL generator will be the default one.

#### Expression Evaluator/Expression Language

[](#expression-evaluatorexpression-language)

- `setExpressionContextVariable($name, $value)`: adds a new expression context variable;
- `setExpressionLanguage(ExpressionLanguage $expressionLanguage)`;

#### (JMS) Serializer Specific

[](#jms-serializer-specific)

- `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 to `addMetadataDir()`, but overrides an existing entry.

Please read the official [Serializer documentation](http://jmsyst.com/libs/serializer) for more details.

#### Others

[](#others)

- `setDebug($debug)`: enables or disables the debug mode;
- `setCacheDir($dir)`: sets the cache directory.

### Configuring a Cache Directory

[](#configuring-a-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();
```

### Configuring Metadata Locations

[](#configuring-metadata-locations)

Hateoas supports several metadata sources. By default, it uses Doctrine annotations (PHP &lt; 8.1) or native PHP attributes (PHP &gt;= 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)`.

### Extending The Library

[](#extending-the-library)

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;
```

Reference
---------

[](#reference)

### XML

[](#xml)

```

                expr(object.getFriends())

```

See the [`hateoas.xsd`](https://github.com/willdurand/Hateoas/blob/master/hateoas.xsd)file for more details.

### YAML

[](#yaml)

```
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())" ]
```

### Annotations

[](#annotations)

#### @Relation

[](#relation)

This annotation can be defined on a class.

Annotation (PHP &lt; 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: '...',
)]
```

PropertyRequiredContentExpression languagenameYesstringNohrefIf embedded is not setstring / [@Route](#route)YesembeddedIf href is not setstring / [@Embedded](#embedded)YesattributesNoarrayYes on valuesexclusionNo[@Exclusion](#exclusion)N/A**Important:** `attributes` are only used on **link relations** (i.e. combined with the `href` property, not with the `embedded` one).

#### @Route

[](#route)

Annotation (PHP &lt; 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](#relation) annotation. This is allows you to your URL generator, if you have configured one.

PropertyRequiredContentExpression languagenameYesstringNoparametersDefaults to array()array / stringYes (string + array values)absoluteDefaults to falseboolean / stringYesgeneratorNostring / nullNo#### @Embedded

[](#embedded)

Annotation (PHP &lt; 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](#relation) annotation. It is useful if you need configure the `exclusion` or `xmlElementName` options for the embedded resource.

PropertyRequiredContentExpression languagecontentYesstring / arrayYes (string)exclusionDefaults to array()[@Exclusion](#exclusion)N/AxmlElementNameDefaults to array()stringNo#### @Exclusion

[](#exclusion)

This annotation can be defined in the **exclusion** property of both the [@Relation](#relation) and [@Embedded](#embedded) annotations.

PropertyRequiredContentExpression languagegroupsNoarrayNosinceVersionNostringNountilVersionNostringNomaxDepthNointegerNoexcludeIfNostring / booleanYesAll 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 &lt; 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 {}
}
```

#### @RelationProvider

[](#relationprovider)

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"}
    ]
  }
}

```

PropertyRequiredContentExpression languagenameYesstringYesIt 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 &lt; 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`.

Internals
---------

[](#internals)

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.

Versioning
----------

[](#versioning)

`willdurand/hateoas` follows [Semantic Versioning](http://semver.org/).

### End Of Life

[](#end-of-life)

As of October 2013, versions `1.x` and `0.x` are officially not supported anymore (note that `1.x` was never released).

### Stable Version

[](#stable-version)

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.

Contributing
------------

[](#contributing)

See CONTRIBUTING file.

Running the Tests
-----------------

[](#running-the-tests)

Install the [Composer](http://getcomposer.org/) `dev` dependencies:

```
php composer.phar install --dev

```

Then, run the test suite using [PHPUnit](http://phpunit.de/):

```
bin/phpunit

```

License
-------

[](#license)

Hateoas is released under the MIT License. See the bundled LICENSE file for details.

###  Health Score

73

—

ExcellentBetter than 100% of packages

Maintenance75

Regular maintenance activity

Popularity71

Solid adoption and visibility

Community45

Growing community involvement

Maturity86

Battle-tested with a long release history

 Bus Factor2

2 contributors hold 50%+ of commits

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~87 days

Recently: every ~108 days

Total

55

Last Release

129d ago

Major Versions

0.0.13 → v2.0.0-alpha12013-10-18

1.0.x-dev → 2.11.02017-05-22

2.0.x-dev → 3.0.0-RC12019-01-11

PHP version history (6 changes)v2.6.0PHP &gt;=5.3

v2.7.0PHP &gt;=5.4

2.11.0PHP ^5.5|^7.0

3.0.0-RC1PHP ^7.2

3.7.0PHP ^7.2 | ^8.0

3.12.0-beta1PHP ^8.1

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1620075?v=4)[François Ganachaud](/maintainers/couac)[@couac](https://github.com/couac)

![](https://avatars.githubusercontent.com/u/776743?v=4)[Asmir Mustafic](/maintainers/goetas)[@goetas](https://github.com/goetas)

---

Top Contributors

[![willdurand](https://avatars.githubusercontent.com/u/217628?v=4)](https://github.com/willdurand "willdurand (289 commits)")[![goetas](https://avatars.githubusercontent.com/u/776743?v=4)](https://github.com/goetas "goetas (112 commits)")[![adrienbrault](https://avatars.githubusercontent.com/u/611271?v=4)](https://github.com/adrienbrault "adrienbrault (88 commits)")[![W0rma](https://avatars.githubusercontent.com/u/20659830?v=4)](https://github.com/W0rma "W0rma (59 commits)")[![ixarlie](https://avatars.githubusercontent.com/u/1876040?v=4)](https://github.com/ixarlie "ixarlie (15 commits)")[![MrHash](https://avatars.githubusercontent.com/u/390925?v=4)](https://github.com/MrHash "MrHash (5 commits)")[![ahilles107](https://avatars.githubusercontent.com/u/298610?v=4)](https://github.com/ahilles107 "ahilles107 (5 commits)")[![Stratadox](https://avatars.githubusercontent.com/u/5333260?v=4)](https://github.com/Stratadox "Stratadox (5 commits)")[![lyrixx](https://avatars.githubusercontent.com/u/408368?v=4)](https://github.com/lyrixx "lyrixx (5 commits)")[![mvhirsch](https://avatars.githubusercontent.com/u/573047?v=4)](https://github.com/mvhirsch "mvhirsch (4 commits)")[![mbabker](https://avatars.githubusercontent.com/u/368545?v=4)](https://github.com/mbabker "mbabker (3 commits)")[![laudeco](https://avatars.githubusercontent.com/u/3658458?v=4)](https://github.com/laudeco "laudeco (3 commits)")[![gimler](https://avatars.githubusercontent.com/u/200904?v=4)](https://github.com/gimler "gimler (3 commits)")[![dmaicher](https://avatars.githubusercontent.com/u/921145?v=4)](https://github.com/dmaicher "dmaicher (2 commits)")[![tams](https://avatars.githubusercontent.com/u/164962?v=4)](https://github.com/tams "tams (2 commits)")[![madesst](https://avatars.githubusercontent.com/u/300694?v=4)](https://github.com/madesst "madesst (2 commits)")[![kayue](https://avatars.githubusercontent.com/u/193112?v=4)](https://github.com/kayue "kayue (2 commits)")[![xavierlacot](https://avatars.githubusercontent.com/u/177293?v=4)](https://github.com/xavierlacot "xavierlacot (1 commits)")[![megazoll](https://avatars.githubusercontent.com/u/347306?v=4)](https://github.com/megazoll "megazoll (1 commits)")[![mrook](https://avatars.githubusercontent.com/u/477719?v=4)](https://github.com/mrook "mrook (1 commits)")

---

Tags

hateoasjsonphprestserializer

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/willdurand-hateoas/health.svg)

```
[![Health](https://phpackages.com/badges/willdurand-hateoas/health.svg)](https://phpackages.com/packages/willdurand-hateoas)
```

###  Alternatives

[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.4k5.6M651](/packages/sylius-sylius)[thecodingmachine/graphqlite

Write your GraphQL queries in simple to write controllers (using webonyx/graphql-php).

5723.1M30](/packages/thecodingmachine-graphqlite)[overblog/graphql-bundle

This bundle provides tools to build a GraphQL server in your Symfony App.

8027.9M28](/packages/overblog-graphql-bundle)[fsc/hateoas-bundle

96215.4k1](/packages/fsc-hateoas-bundle)[ae/salesforce-rest-sdk

An SDK for the Salesforce Rest API

21118.3k1](/packages/ae-salesforce-rest-sdk)[sourcebroker/t3api

REST API for your TYPO3 project. Config with annotations, build in filtering, pagination, typolinks, image processing, serialization contexts, responses in Hydra/JSON-LD format.

3880.8k4](/packages/sourcebroker-t3api)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
