Generating Resources from PHP Objects

In the previous chapter, we discussed links and resources. The primitive objects allow us to create representations easily, but do not answer one critical question: how can we create resources based on existing PHP object types?

To answer that question, we provide two related features: metadata, and a resource generator.

Metadata

Metadata allows you to detail the requirements for generating a HAL representation of a PHP object. Metadata might include:

All metadata types inherit from Hal\Metadata\AbstractMetadata, which defines a single method, getClass(), for retrieving the name of the PHP class to represent; all metadata are expected to inherit from this class.

The component also provides four concrete metadata types, requiring the following information:

We aggregate metadata in a Hal\Metadata\MetadataMap instance:

$bookMetadata = new RouteBasedResourceMetadata(
    Book::class,
    'book',
    ObjectPropertyHydrator::class
);
$booksMetadata = new RouteBasedCollectionMetadata(
    BookCollection::class,
    'book',
    'books',
);

$metadataMap = new MetadataMap();
$metadataMap->add($bookMetadata);
$metadataMap->add($booksMetadata);

Configuration-based metadata

To automate generation of the MetadataMap, we provide Hal\Metadata\MetadataMapFactory. This factory may be used with any PSR-11 container. It utilizes the config service, and pulls its configuration from a key named after the Hal\Metadata\MetadataMap class.

Each item in the map will be an associative array. The member __class__ will describe which metadata class to create, and the remaining properties will then be used to generate an instance. As an example, the above could be configured as follows:

use Api\Books\Book;
use Api\Books\BookCollection;
use Hal\Metadata\MetadataMap;
use Hal\Metadata\RouteBasedCollectionMetadata;
use Hal\Metadata\RouteBasedResourceMetadata;
use Zend\Hydrator\ObjectProperty;

return [
    'Hal\Metadata\MetadataMap' => [
        [
            '__class__' => RouteBasedResourceMetadata::class,
            'resource_class' => Book::class,
            'route' => 'book',
            'extractor' => ObjectProperty::class,
        ],
        [
            '__class__' => RouteBasedCollectionMetadata::class,
            'collection_class' => BookCollection::class,
            'collection_relation' => 'book',
            'route' => 'books',
        ],
    ],
];

ResourceGenerator

Once you have defined the metadata for the various objects you will represent in your API, you can start generating resources.

Hal\ResourceGenerator has the following constructor:

public function __construct(
    Hal\Metadata\MetadataMap $metadataMap,
    Psr\Container\ContainerInterface $hydrators,
    Hal\LinkGenerator $linkGenerator
) {

We described the MetadataMap in the previous section, and the LinkGenerator in the previous chapter.

Hydrators are defind in the zend-hydrator component, and are objects which can hdyrate associative arrays to object instances and extract associative arrays from object instances. Generally speaking, the $hydrators instance may be any PSR-11 container, but you will generally want to use the Zend\Hydrator\HydratorPluginManager.

Once you have your instance created, you can start generating resources:

$resource = $resourceGenerator->fromObject($book, $request);

(Where $request is a Psr\Http\Message\ServerRequestInterface instance; the instance is passed along to the LinkGenerator in order to generate route-based URIs for Link instances.)

Customizing resource generation

The ResourceGenerator allows composing Hal\ResourceGenerator\Strategy instances. The Strategy interface defines the following:

namespace Hal\ResourceGenerator;

use Hal\HalResource;
use Hal\Metadata;
use Hal\ResourceGenerator;
use Psr\Http\Message\ServerRequestInterface;

interface Strategy
{
    /**
     * @param object $instance Instance from which to create Resource.
     * @throws Exception\UnexpectedMetadataTypeException for metadata types the
     *     strategy cannot handle.
     */
    public function createResource(
        $instance,
        Metadata\AbstractMetadata $metadata,
        ResourceGenerator $resourceGenerator,
        ServerRequestInterface $request
    ) : HalResource;
}

When you register a strategy, you will map a metadata type to the strategy; the ResourceGenerator will then call your strategy whenever it encounteres metadata of that type.

$resourceGenerator->addStrategy(CustomMetadata::class, CustomStrategy::class);

// or:
$resourceGenerator->addStrategy(CustomMetadata::class, $strategyInstance);

If a strategy already is mapped for the given metadata type, this method will override it.

To facilitate common operations, this library provides two traits, Hal\ResourceGenerator\ExtractCollection and Hal\ResourceGenerator\ExtractInstance; inspect these if you decide to write your own strategies.