weierophinney/hal

This component provides tools for generating Hypertext Application Language (HAL) payloads for your APIs, in both JSON and XML formats.

At its core, it features:

These two tools allow you to model payloads of varying complexity.

To allow providing representations of these, we provide Hal\HalResponseFactory. This factory generates a PSR-7 response for the provided resource, including its links and any embedded/child resources it composes.

Creating link URIs by hand is error-prone, as URI schemas may change; most frameworks provide route-based URI generation for this reason. To address this, we provide Hal\LinkGenerator, and an accompanying interface, Hal\LinkGenerator\UrlGenerator. You may use these to generate Link instances that use URIs based on routes you have defined in your application. We also ship Hal\LinkGenerator\ExpressiveUrlGenerator, which provides a UrlGenerator implementation backed by the zend-expressive-helpers package.

Finally, we recognize that most modern PHP applications use strong data modeling, and thus API payloads need to represent PHP objects. To facilitate this, we provide two components:

The purpose of the package is to automate creation of HAL payloads, including relational links, from PHP objects.

Installation

Use Composer:

$ composer require weierophinney/hal

If you are adding this to an Expressive application, and have the zend-component-installer package installed, this will prompt you to ask if you wish to add it to your application configuration; please do, as the package provides a number of useful factories.

We also recommend installing zend-hydrator, which provides facilities for extracting associative array representations of PHP objects:

$ composer require zendframework/zend-hydrator

Finally, if you want to provide paginated collections, we recommend installing zend-paginator:

$ composer require zendframework/zend-paginator

Quick Start

The following examples assume that you have added this package to an Expressive application.

Entity and collection classes

For each of our examples, we'll assume the following class exists:

namespace Api\Books;

class Book
{
    public $id;
    public $title;
    public $author;
}

Additionally, we'll have a class representing a paginated group of books:

namespace Api\Books;

use Zend\Paginator\Paginator;

class BookCollection extends Paginator
{
}

Routes

The examples below assume that we have the following routes defined in our application somehow:

Create metadata

In order to allow creating representations of these classes, we need to provide the resource generator with metadata describing them. This is done via configuration, which you could put in one of the following places:

The configuration will look like this:

// Provide the following imports:
use Api\Books\Book;
use Api\Books\BookCollection;
use Hal\Metadata\MetadataMap;
use Hal\Metadata\RouteBasedCollectionMetadata;
use Hal\Metadata\RouteBasedResourceMetadata;
use Zend\Hydrator\ObjectProperty as ObjectPropertyHydrator;

// And include the following in your configuration:
MetadataMap::class => [
    [
        '__class__' => RouteBasedResourceMetadata::class,
        'resource_class' => Book::class,
        'route' => 'book',
        'extractor' => ObjectPropertyHydrator::class,
    ],
    [
        '__class__' => RouteBasedCollectionMetadata::class,
        'collection_class' => BookCollection::class,
        'collection_relation' => 'book',
        'route' => 'books',
    ],
],

Manually creating and rendering a resource

The following middleware creates a HalResource with its associated links, and then manually renders it using Hal\Renderer\JsonRenderer. (An XmlRenderer is also provided, but not demonstrated here.)

We'll assume that Api\Books\Repository handles retrieving data from persistent storage.

namespace Api\Books\Action;

use Api\Books\Repository;
use Hal\HalResource;
use Hal\Link;
use Hal\Renderer\JsonRenderer;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;
use Zend\Diactoros\Response\TextResponse;
class BookAction implements MiddlewareInterface
{
    /** @var JsonRenderer */
    private $renderer;
    /** @var Repository */
    private $repository;
    public function __construct(
        Repository $repository,
        JsonRenderer $renderer
    ) {
        $this->repository = $repository;
        $this->renderer = $renderer;
    }
    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $id = $request->getAttribute('id', false);
        if (! $id) {
            throw new RuntimeException('No book identifier provided', 400);
        }
        $book = $this->repository->get($id);
        $resource = new HalResource((array) $book);
        $resource = $resource->withLink(new Link('self'));
        return new TextResponse(
            $this->renderer->render($resource),
            200,
            ['Content-Type' => 'application/hal+json']
        );
    }
}

The JsonRenderer returns the JSON string representing the data and links in the resource. The payload generated might look like the following:

{
    "_links": {
        "self": { "href": "/api/books/1234" }
    },
    "id": 1234,
    "title": "Hitchhiker's Guide to the Galaxy",
    "author": "Adams, Douglas"
}

The above example uses no metadata, and manually creates the HalResource instance. As the complexity of your objects increase, and the number of objects you want to represent via HAL increases, you may not want to manually generate them.

Middleware using the ResourceGenerator and ResponseFactory

In this next example, our middleware will compose a Hal\ResourceGenerator instance for generating a Hal\HalResource from our objects, and a Hal\HalResponseFactory for creating a response based on the returned resource.

First, we'll look at middleware that displays a single book. We'll assume that Api\Books\Repository handles retrieving data from persistent storage.

namespace Api\Books\Action;

use Api\Books\Repository;
use Hal\HalResponseFactory;
use Hal\ResourceGenerator;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\ServerRequestInterface;
use RuntimeException;

class BookAction
{
    /** @var Repository */
    private $repository;

    /** @var ResourceGenerator */
    private $resourceGenerator;

    /** @var HalResponseFactory */
    private $responseFactory;

    public function __construct(
        Repository $repository,
        ResourceGenerator $resourceGenerator,
        HalResponseFactory $responseFactory
    ) {
        $this->repository = $repository;
        $this->resourceGenerator = $resourceGenerator;
        $this->responseFactory = $responseFactory;
    }

    public function __invoke(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $id = $request->getAttribute('id', false);
        if (! $id) {
            throw new RuntimeException('No book identifier provided', 400);
        }

        /** @var \Api\Books\Book $book */
        $book = $this->repository->get($id);

        $resource = $this->resourceGenerator->fromObject($book, $request);
        return $this->responseFactory->createResponse($request, $resource);
    }
}

Note that the $request instance is passed to both the resource generator and response factory:

The generated payload might look like the following:

{
    "_links": {
        "self": { "href": "/api/books/1234" }
    },
    "id": 1234,
    "title": "Hitchhiker's Guide to the Galaxy",
    "author": "Adams, Douglas"
}

Middleware returning a collection

Next, we'll create middleware that returns a collection of books. The collection will be paginated (assume our repository class creates a BookCollection backed by an appropriate adapter), and use a query string parameter to determine which page of results to return.

namespace Api\Books\Action;

use Api\Books\Repository;
use Hal\HalResponseFactory;
use Hal\ResourceGenerator;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\ServerRequestInterface;
use RuntimeException;

class BooksAction
{
    /** @var Repository */
    private $repository;

    /** @var ResourceGenerator */
    private $resourceGenerator;

    /** @var HalResponseFactory */
    private $responseFactory;

    public function __construct(
        Repository $repository,
        ResourceGenerator $resourceGenerator,
        HalResponseFactory $responseFactory
    ) {
        $this->repository = $repository;
        $this->resourceGenerator = $resourceGenerator;
        $this->responseFactory = $responseFactory;
    }

    public function __invoke(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $page = $request->getQueryParams()['page'] ?? 1;

        /** @var \Api\Books\BookCollection $books */
        $books = $this->repository->fetchAll();

        $books->setItemCountPerPage(25);
        $books->setCurrentPageNumber($page);

        $resource = $this->resourceGenerator->fromObject($books, $request);
        return $this->responseFactory->createResponse($request, $resource);
    }
}

Note that resource and response generation is exactly the same as our previous example! This is because the metadata map takes care of the details of extracting the data from our value objects and generating links for us.

In this particular example, since we are using a paginator for our collection class, we might get back something like the following:

{
    "_links": {
        "self": { "href": "/api/books?page=7" },
        "first": { "href": "/api/books?page=1" },
        "prev": { "href": "/api/books?page=6" },
        "next": { "href": "/api/books?page=8" },
        "last": { "href": "/api/books?page=17" }
        "search": {
            "href": "/api/books?query={searchTerms}",
            "templated": true
        }
    },
    "_embedded": {
        "book": [
            {
                "_links": {
                    "self": { "href": "/api/books/1234" }
                }
                "id": 1234,
                "title": "Hitchhiker's Guide to the Galaxy",
                "author": "Adams, Douglas"
            },
            {
                "_links": {
                    "self": { "href": "/api/books/6789" }
                }
                "id": 6789,
                "title": "Ancillary Justice",
                "author": "Leckie, Ann"
            },
            /* ... */
        ]
    },
    "_page": 7,
    "_per_page": 25,
    "_total": 407
}

Next steps

The above examples demonstrate setting up your application to generate and return HAL resources. In the following chapters, we'll cover: