Giter VIP home page Giter VIP logo

hateoas's Introduction

Hateoas

GitHub Actions GitHub Actions Latest Stable Version PHP Version Require

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

Installation

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.

Working With Symfony

There is a bundle for that! Install the BazingaHateoasBundle, and enjoy!

Usage

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.

Introduction

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.

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 section.

A link is a relation which is identified by a name (e.g. self) and that has an href parameter:

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 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.

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.

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

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.

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 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&amp;limit=10" />
    <link rel="first" href="/api/users?page=1&amp;limit=10" />
    <link rel="last" href="/api/users?page=1&amp;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:

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

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.

VndErrorRepresentation

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.

The Expression Language

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:

/**
 * @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.

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

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

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

URL Generators

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:

use Hateoas\Configuration\Annotation as Hateoas;

/**
 * @Hateoas\Relation(
 *      "self",
 *      href = @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

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

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 feature above is also available in your expressions (cf. The Expression Language) through the link(object, rel, absolute) function:

/**
 * @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);
    }
}

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

Hateoas also provides a set of Twig extensions.

LinkExtension

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 #}

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, or Atom Links for instance.

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.

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

The XmlSerializer

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

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>

Adding New Serializers

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

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

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

JSON Serializer

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

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

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

(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 for more details.

Others

  • setDebug($debug): enables or disables the debug mode;
  • setCacheDir($dir): sets the 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

Hateoas supports several metadata sources. By default, it uses Doctrine annotations, 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

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

XML

<?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.

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

@Relation

This annotation can be defined on a class.

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).

@Route

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"
 *     )
 * )
 */

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

@Embedded

use Hateoas\Configuration\Annotation as Hateoas;

/**
 * @Hateoas\Relation(
 *     name = "friends",
 *     embedded = @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

@Exclusion

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:

/**
 * @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() {}
}

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

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

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

willdurand/hateoas follows Semantic Versioning.

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

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

See CONTRIBUTING file.

Running the Tests

Install the Composer dev dependencies:

php composer.phar install --dev

Then, run the test suite using PHPUnit:

bin/phpunit

License

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

hateoas's People

Contributors

adrienbrault avatar ahilles107 avatar fsevestre avatar gimler avatar goetas avatar kayue avatar laudeco avatar lyrixx avatar madesst avatar mbabker avatar megazoll avatar mrhash avatar mrook avatar ninir avatar nyedidikeke avatar pborreli avatar piotrantosik avatar rogerb87 avatar romainneutron avatar ruudk avatar scrutinizer-auto-fixer avatar slaat avatar smoke avatar stratadox avatar tals56 avatar tams avatar w0rma avatar weaverryan avatar willdurand avatar xavierlacot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

hateoas's Issues

2.0.0

@adrienbrault I'd like to release Hateoas 2.0.0, WDYT?

I have a few comments on the current base code:

  • I don't think LinksFactory vs LinkFactory is perfect, but we can live with that (I don't have better names anyway);
  • I don't like the Hateoas\Handler sub-namespace, it is not clear and I don't get it;
  • Why Relation rather than Link?
  • What is the RelationsRepository?
  • We could rename the EmbedsFactory to its singular form but we need to find better names for (Links|Link)Factory.
  • No documentation!!

[RFC] Made Builder::setUrlGenerator simpler

From the doc

$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()
;

As everybody will use the default routeur, I suggest:

$hateoas = HateoasBuilder::create()
    ->setUrlGenerator(
        new CallableUrlGenerator(function ($route, array $parameters, $absolute) use ($myFramework) {
            return $myFramework->generateTheUrl($route, $parameters, $absolute);
        }),
        null, // Optionnel, By default all links uses the generator configured with the null name
    )
    ->build()
;

ResourceLinkDefinition parameter verbose

I currently have to specify the parameters argument as shown when creating a resource link definition:

$linkDef = new RouteLinkDefinition('cats', array('page' => array('page' => 'CurrentPageNumber')), Link::REL_SELF);

Is this a required format? Could it be possible to specify as follows?

$linkDef = new RouteLinkDefinition('cats', array('page' => 'CurrentPageNumber')), Link::REL_SELF);

Doing as shown currently generates a route as http://www.example.com/cats/?CurrentPageNumber=1 when http://www.example.com/cats/?page=1 is what i would have expected.

Serialization listeners

I think it would be useful to register serialization listeners for resources such that data can be manipulated before serialization but within the application context. This requirement has arisen for me because associations are not available in Doctrine 2 postLoad events, but the usefulness of this feature is broader as it enables any kind of decoupled entity transformation prior to serialization.

See a724f64 for implementation.

Getting a relation link of an object

The use case:
Create an object with POST request and save it to the database. The API should return HTTP 201 with a "Location: http://example.com/resource/123" header.

Of course I can just generate the url with framework's url generator, but I'd like to reuse object's "_self" link and not duplicate parameters substitution.

$link = $app['hateoas']->getLink($object, '_self');

Does this make sense? Would you merger a similar PR?

Embedding and hrefs definition duplication

When embedding a relation, you need to duplicate the href definition in both the embedded class and the class embedding:

/**
 * @Relation(
 *     "post",
 *     href = @Route(
 *         "post_get",
 *         parameters = {
 *             "id" = "expr(object.getPost().getId())"
 *         }
 *     )
 * )
 */
class User {}

/**
 * @Relation(
 *     "self",
 *     href = @Route(
 *         "post_get",
 *         parameters = {
 *             "id" = "expr(object.getId())"
 *         }
 *     )
 * )
 */
class Post {}

It would be nice to have an expression language function to get an object link:

/**
 * @Relation(
 *     "post",
 *     href = "expr(link(object.getPost(), 'self'))"
 * )
 */
class User {}

/**
 * @Relation(
 *     "self",
 *     href = @Route(
 *         "post_get",
 *         parameters = {
 *             "id" = "expr(object.getId())"
 *         }
 *     )
 * )
 */
class Post {}

Correctly handle inlining with links and embeds with json

So, imagine the following use case (though this exact one does not really make sense):

/**
 * @Hateoas\Relation(...)
 */
class Collection
{

}
/**
 * @Hateoas\Relation(...)
 */
class UserCollection
{
    /** @Serializer\Inline */
    private $collection;
}
$serializer->serialize(new UserCollection(new Collection), 'json')

The issue is that the JsonEventSubscriber will first call Visitor::addData('links', ...) for the Collection links, and will later call Visitor::addData('links', ...) for the UserCollection links ... And it will throw an exception because it will still be the same JSON object to which it's trying to add links ...

This is taken care of in the FSCHateoasBundle, I'm creating the issue to not forget.

Relation href requirement

Currently we have to specify an href for each relation we add.
The issue is that there are some cases where a link does not make sense, for example collections in HAL.

So a link should either have an href, an embed, or both.

Exclusions

So, I have 2 exclusions cases in mind:

/**
 * @Relation(
 *     "manager",
 *     href = @Route(
 *         "user_get",
 *         parameters = { "id" = "expr(object.manager.id)" }
 *     )
 * )
 */
class User
{
    private $manager;
}

This should fail if the manager is null. We need a way to avoid that. The FSCHateoasBundle has an exclude_if key that will prevent links/embeds when the check fails.

The second case is views: exactly like with the serializer you would use groups to limit the output on a collection vs a single resource endpoint, we should be able to do the same. I am not really sure but I think groups should only apply to embeds.

Metadata extension point

So,

I think we should think about a clean way for other libraries/bundles to add relations (without the user having to specify @RelationProvider).

The issue with the metadata library, is that it will only use the first Driver class metadata and discard all the other one. See https://github.com/schmittjoh/metadata/blob/master/src/Metadata/Driver/DriverChain.php#L32-L38 ping @schmittjoh
So if FOSRestBundle for example wanted to add relations on classes, how should it be done ? ping @lsmith77

PagerfantaFactory should allow custom $inline

Currently the PagerfantaFactory will just pass the pager current pager results:
https://github.com/willdurand/Hateoas/blob/master/src/Hateoas/Representation/Factory/PagerfantaFactory.php#L32

However, in most cases, we want to be able to have a https://github.com/willdurand/Hateoas/blob/master/src/Hateoas/Representation/Collection.php instance instead.

So the factory parameters should allow an optional one to define the inline.

(Damn documentation that finds all this stuff!)

Serialization error

Trying to serialize a simple resource/link throws this error:

Catchable fatal error: Argument 3 passed to JMS\Serializer\GraphNavigator::accept() must be an instance of JMS\Serializer\Context, instance of JMS\Serializer\XmlSerializationVisitor given

Not sure if i'm doing the serialization correctly given that you have not provided an explicit example.

ommiting links and embedded on null

Having the relation definition like in the example should work and probably skip the complete relation in a cases where expr(object.getLocation()) returns null.
ATM the link generation is failing with Unable to get a property on a non-object. and embed produces an empty node.

<h:relation rel="http://api.soaringspot.com/rel/location">
    <h:href route="get_location" absolute="true">
        <h:parameter name="slug" value="expr(object.getLocation().getId())"/>
    </h:href>
    <h:embed xml-element-name="location">
        <h:content>expr(object.getLocation())</h:content>
        <h:exclusion groups="public"/>
    </h:embed>
    <h:exclusion groups="public"/>
</h:relation>

Link display conditions

Currently the factory definition can configure the output with pre-specificed link definitions. I was wondering if it makes sense to have a display condition on the definition so that in situations where the link should not be shown it can be skipped. The case in mind is where the 'next' relations is the same as the 'self'. Since we can specify a 'last' relation we can put some logic in the client to compute the need to look up the 'next' relation. However it would be simpler i think if the 'next' relation simply didn't appear in the output, and vice versa the 'previous' relation.

Another example may be where a resource has a sub collection referenced by a link. If that collection is known to be empty is there any need to show the link?

Collection* Renaming

SimpleCollection and PaginatedCollection are not the same thing, and PaginatedCollection would almost always be used with SimpleCollection:

new PaginatedCollection(
    new SimpleCollection(
        $orders,
        'orders'
    ),
    'get_orders',
)
  • PaginatedCollection just add links
  • SimpleCollection make sure the resources are serialized in the _embedded object with the appropriate rel

So I would suggest renaming:

  • SimpleCollection to CollectionRepresentation
  • PaginatedCollection to PaginatedRepresentation

The above example would then become:

new PaginatedRepresentation(
    new CollectionRepresentation(
        $orders,
        'orders'
    ),
    'get_orders',
)

Remove PagerfantaFactory

PagerfantaFactory is only useful to configure the PaginatedCollection $pageParameterName and $limitParameterName.

The PaginatedCollection could probably use values from the expression language.

Documentation

The idea is to do something like the serializer doc: have everything in detail in the annotations reference.

  • Explain that embeds use the @Serializer\XmlRoot for the xml element names
  • Expression Language reference
  • Expression Evaluator context
  • Caching configuration for a non symfony app ?
  • Document the existing json/xml serializers, and how to add new ones
  • ConfigurationExtensionInterface
  • XML config reference
  • HateoasBuilder
  • Add PagerFanta example (code)

Clarification on resource and collection types

On the subject of documentation again, you've used application/vnd.acme.comment for a resource and collection in one example, but in another example you have specified application/vnd.acme.user for a resource and application/vnd.acme.users for a collection. Which is the correct usage? I realise this is not a code issue but would appreciate the clarification.

JSON-LD serializer

I haven't looked much into JSON-LD, but what confuses me is how you can mix the "schema/semantic" within the document.

Anyway, I think it should be possible to at least support @id.

Metadata cascading?

When a class is configured through YAML (for the Serializer part), adding Hateoas configuration through annotations does not work.

@adrienbrault is it an expected behavior?

Http 'method' in links?

Hello,

In some presentations they make following links in their response:

<link method="get" rel="self" href="http://local/products/1" />
<link method="delete" rel="delete" href="http://local/products/1" />

I didn't see a support for that in Hateoas ... is there a specific reason? Is it 'RESTful' friendly?

Cheers

YamlConfig is not compatible with Symfony\Yaml 2.3

public function __construct($stringOrFile)
{
$config = Yaml::parse($stringOrFile);
if (isset($config['hateoas'])) {
$this->config = $config['hateoas'];
}
}

Symfony\Yaml remove support of parsing file. It only support parsing of string.

Do you want a PR ? What's your strategy : We do the file_get_content or the end user do the file_get_contents ?

I'm πŸ‘ for "we do". And so, we will be able to cache the result as a parsing is ALWAYS expensive.

New handlers

I was thinking that we could have a GlobalVariablesHandler, and a ConcatenateHandler, to be able to set the base rel in a configuration:

bazinga_hateoas:
    globals:
        base_rel: https://rels.website.com
links:
    -   rel: @concat. @global.base_rel /users
        href: whatever

However we should probably decide on a better formatting than the current one, it's weird with my example.
It's a good thing 2.0 is not released yet, we could move away from the @handler_name.xxx current format πŸ˜ƒ .
Maybe a more advanced format with operators, the possibility to call methods etc ? (would be nice if we had a Service.xxx handle ;-), especially for the embed: config) Could be cached like doctrine DQL. WDYT ?

LinkProviderInterface

In the project i am working on, I have models that needs some dynamic links.

I would like to be able to do the following:

class Product implements LinkProviderInterface
{
    public function createLinks(RouteFactoryInterface $routeFactory)
    {
        $links = array();
        foreach ($this->getImages() as $image) {
            $links[] = new Link(
                '/images',
                $routeFactory->create(
                    'image_get', array(
                        'id' => $image->getId()
                    )
                )
            );
        }

        return $links;
    }
}

I think using an interface is simpler to implement and to use.

Another interface could exist to add embeds.

WDYT about adding this feature ?

Integration with Serializer/JMSSerializerBundle/Symfony2

So I played a bit with the upcoming version of this lib. Amazing!

However, I am not sure about how to integrate it into Symfony2. What I wanted to do was to inject the serializer_builder service into the HateoasBuilder, and then replace the serializer service with an Hateoas instance.

Thing is, the serializer_builder service does not exist anymore in JMSSerializerBundle. In my opinion, it was useful as it contained all the configuration, and that was an easy way to "extend" the serializer. ping @schmittjoh

So, another option would be to add all Hateoas handlers/subscribers/whatever to the serializer service itself. The bundle would just create a few non-public services in the DIC with the right tags. Not sure it will be enough though..

About Hateoas itself, the configuration should be fairly easy. All we need to do is to inject the right route factory (aka SymfonyRouteFactory), and adding the metadata directories.

So what would be the best option?

(I started https://github.com/willdurand/BazingaHateoasBundle)

Missing serialization configuration files for Link?

I know the docs are not complete, but are you missing something like this:

<serializer>
    <class name="Hateoas\Link" xml-root-name="link">
        <property name="href" xml-attribute="true"></property>
        <property name="rel" xml-attribute="true"></property>
        <property name="type" xml-attribute="true"></property>
    </class>
</serializer>

?

Would be great to update the docs with complete serializer config files.

Embedded resources

Does this library support embedding of resources?

I'd like to eventually contribute to this library.

I created my own a while ago, but I like where you're headed!.. Before I refractor our API to this library, embedding resources is a must. Do you see this being a part of this library?

https://github.com/j/hal

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.