Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
Ready for 2.2.0
  • Loading branch information
weierophinney committed Jul 25, 2013
2 parents 856895f + 9e79bc7 commit 189b471
Show file tree
Hide file tree
Showing 7 changed files with 386 additions and 19 deletions.
52 changes: 49 additions & 3 deletions docs/ref/advanced-rendering.rst
Expand Up @@ -25,7 +25,8 @@ However, it does expose a few pieces of functionality that may be of interest:
URLs (i.e., contains schema, hostname, and port, in addition to path). This
was detailed :ref:`in the previous section <ref/advanced-routing>`.
- The ``getIdFromResource`` event (also detailed :ref:`in the previous section
<ref/advanced-routing>`).
<ref/advanced-routing>`).
- The ``renderResource`` and ``renderCollection`` events.
- The ``renderCollection.resource`` event.

If in a controller, or interacting with a controller instance, you can access it
Expand All @@ -36,8 +37,53 @@ via the controller's ``plugin()`` method:
$halLinks = $controller->plugin('HalLinks');
For the purposes of this chapter, we'll look specifically at the
``renderCollection.resource`` event, as it allows you, the developer, to fully
customize how you extract your resource to an array.
``renderResource``, ``renderCollection``, and ``renderCollection.resource``
events, as they allow you, the developer, to fully customize how you extract
your resource to an array as well as manipulate links.

The renderResource and renderCollection events
----------------------------------------------

These events are triggered each time you render a resource or collection, even
if they are embedded. As such, this allows you to introspect these items and
manipulate them prior to extracting them to a representation.

As an example, let's say we want to inject a "describedby" link into any
``My\User`` resource and/or ``My\Users`` collection. We can do this as follows:

.. code-block:: php
:linenos:
$sharedEvents->attach(
'PhlyRestfully\Plugin\HalLinks',
array('renderResource', 'renderCollection'),
function ($e) {
$resource = $e->getParam('resource', false);
$collection = $e->getParam('collection', false);
if (!$resource && !$collection) {
return;
}
if ($resource && !$resource instanceof \My\User) {
return;
}
if ($collection && !$collection instanceof \My\Users) {
return;
}
if ($collection) {
$resource = $collection;
}
$links = $resource->getLinks();
$links->add(\PhlyRestfully\Link::factory(array(
'rel' => 'describedby',
'url' => 'http://example.com/api/help/resources/user',
)));
}
);
The above attaches to both events, and then checks if we have a resource or
collection we're interested in; if so, it creates a link and injects it into the
composed link collection. This will ensure we have a "describedby" relational
link in each one of these rendered resources.

The renderCollection.resource event
-----------------------------------
Expand Down
5 changes: 5 additions & 0 deletions docs/ref/metadata-map.rst
Expand Up @@ -65,6 +65,11 @@ The following options are available for metadata maps:
defaults to "id". (**OPTIONAL**)
- **is_collection**: boolean flag indicating whether or not the resource is a
collection; defaults to "false". (**OPTIONAL**)
- **links**: array of additional relational links to use with the resource or
collection. Each item in the array is itself an array, with the required key
"rel" (describing the relation), and one of either "url" (a string) or "route"
(an array with the members: "name", required; "params", an array, optional;
and "options", an array, optional). (**OPTIONAL**)
- **resource_route**: the name of the route to use for resources embedded as part
of a collection. If not set, the route for the resource is used. (**OPTIONAL**)
- **route**: the name of the route to use for generating the "self" relational
Expand Down
53 changes: 53 additions & 0 deletions src/PhlyRestfully/Link.php
Expand Up @@ -54,6 +54,59 @@ public function __construct($relation)
$this->relation = (string) $relation;
}

/**
* Factory for creating links
*
* @param array $spec
* @return self
* @throws Exception\InvalidArgumentException if missing a "rel" or invalid route specifications
*/
public static function factory(array $spec)
{
if (!isset($spec['rel'])) {
throw new Exception\InvalidArgumentException(sprintf(
'%s requires that the specification array contain a "rel" element; none found',
__METHOD__
));
}
$link = new static($spec['rel']);

if (isset($spec['url'])) {
$link->setUrl($spec['url']);
return $link;
}

if (isset($spec['route'])) {
$routeInfo = $spec['route'];
if (is_string($routeInfo)) {
$link->setRoute($routeInfo);
return $link;
}

if (!is_array($routeInfo)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s requires that the specification array\'s "route" element be a string or array; received "%s"',
__METHOD__,
(is_object($routeInfo) ? get_class($routeInfo) : gettype($routeInfo))
));
}

if (!isset($routeInfo['name'])) {
throw new Exception\InvalidArgumentException(sprintf(
'%s requires that the specification array\'s "route" array contain a "name" element; none found',
__METHOD__
));
}
$name = $routeInfo['name'];
$params = isset($routeInfo['params']) && is_array($routeInfo['params']) ? $routeInfo['params'] : array();
$options = isset($routeInfo['options']) && is_array($routeInfo['options']) ? $routeInfo['options'] : array();
$link->setRoute($name, $params, $options);
return $link;
}

return $link;
}

/**
* Set the route to use when generating the relation URI
*
Expand Down
38 changes: 38 additions & 0 deletions src/PhlyRestfully/Metadata.php
Expand Up @@ -46,6 +46,13 @@ class Metadata
*/
protected $isCollection = false;

/**
* Collection of additional relational links to inject in resource
*
* @var array
*/
protected $links = array();

/**
* Route for resources composed in a collection
*
Expand Down Expand Up @@ -154,6 +161,16 @@ public function getIdentifierName()
return $this->identifierName;
}

/**
* Retrieve set of relational links to inject, if any
*
* @return array
*/
public function getLinks()
{
return $this->links;
}

/**
* Retrieve the resource route
*
Expand Down Expand Up @@ -312,6 +329,27 @@ public function setIsCollection($flag)
return $this;
}

/**
* Set relational links.
*
* Each element in the array should be an array with the elements:
*
* - rel - the link relation
* - url - the URL to use for the link OR
* - route - an array of route information for generating the link; this
* should include the elements "name" (required; the route name),
* "params" (optional; additional parameters to inject), and "options"
* (optional; additional options to pass to the router for assembly)
*
* @param array $links
* @return self
*/
public function setLinks(array $links)
{
$this->links = $links;
return $this;
}

/**
* Set the resource route (for embedded resources in collections)
*
Expand Down
65 changes: 52 additions & 13 deletions src/PhlyRestfully/Plugin/HalLinks.php
Expand Up @@ -317,6 +317,7 @@ public function getHydratorForResource($resource)
*/
public function renderCollection(HalCollection $halCollection)
{
$this->getEventManager()->trigger(__FUNCTION__, $this, array('collection' => $halCollection));
$collection = $halCollection->collection;
$collectionName = $halCollection->collectionName;

Expand Down Expand Up @@ -349,6 +350,7 @@ public function renderCollection(HalCollection $halCollection)
*/
public function renderResource(HalResource $halResource)
{
$this->getEventManager()->trigger(__FUNCTION__, $this, array('resource' => $halResource));
$resource = $halResource->resource;
$id = $halResource->id;
$links = $this->fromResource($halResource);
Expand Down Expand Up @@ -543,20 +545,13 @@ public function createResourceFromMetadata($object, Metadata $metadata)
$id = $data[$identiferName];

$resource = new HalResource($data, $id);

$link = new Link('self');
if ($metadata->hasRoute()) {
$params = array_merge($metadata->getRouteParams(), array($identiferName => $id));
$link->setRoute($metadata->getRoute(), $params, $metadata->getRouteOptions());
} elseif ($metadata->hasUrl()) {
$link->setUrl($metadata->getUrl());
} else {
throw new Exception\RuntimeException(sprintf(
'Unable to create a self link for resource of type "%s"; metadata does not contain a route or a url',
get_class($object)
));
$links = $resource->getLinks();
$this->marshalMetadataLinks($metadata, $links);
if (!$links->has('self')) {
$link = $this->marshalSelfLinkFromMetadata($metadata, $object, $id, $identiferName);
$links->add($link);
}
$resource->getLinks()->add($link);

return $resource;
}

Expand Down Expand Up @@ -624,6 +619,7 @@ public function createCollectionFromMetadata($object, Metadata $metadata)
$collection->setCollectionRoute($metadata->getRoute());
$collection->setResourceRoute($metadata->getResourceRoute());
$collection->setIdentifierName($metadata->getIdentifierName());
$this->marshalMetadataLinks($metadata, $collection->getLinks());
return $collection;
}

Expand Down Expand Up @@ -893,4 +889,47 @@ protected function convertResourceToArray($resource)
return $hydrator->extract($resource);
}

/**
* Creates a link object, given metadata and a resource
*
* @param Metadata $metadata
* @param object $object
* @param string $id
* @param string $identifierName
* @return Link
* @throws Exception\RuntimeException
*/
protected function marshalSelfLinkFromMetadata(Metadata $metadata, $object, $id, $identifierName)
{
$link = new Link('self');
if ($metadata->hasUrl()) {
$link->setUrl($metadata->getUrl());
return $link;
}

if (!$metadata->hasRoute()) {
throw new Exception\RuntimeException(sprintf(
'Unable to create a self link for resource of type "%s"; metadata does not contain a route or a url',
get_class($object)
));
}

$params = array_merge($metadata->getRouteParams(), array($identifierName => $id));
$link->setRoute($metadata->getRoute(), $params, $metadata->getRouteOptions());
return $link;
}

/**
* Inject any links found in the metadata into the resource's link collection
*
* @param Metadata $metadata
* @param LinkCollection $links
*/
protected function marshalMetadataLinks(Metadata $metadata, LinkCollection $links)
{
foreach ($metadata->getLinks() as $linkData) {
$link = Link::factory($linkData);
$links->add($link);
}
}
}
43 changes: 42 additions & 1 deletion test/PhlyRestfullyTest/LinkTest.php
Expand Up @@ -13,7 +13,7 @@

class LinkTest extends TestCase
{
public function testConstructorTakesLinkReationName()
public function testConstructorTakesLinkRelationName()
{
$link = new Link('describedby');
$this->assertEquals('describedby', $link->getRelation());
Expand Down Expand Up @@ -140,4 +140,45 @@ public function testIsCompleteReturnsTrueWhenRouteIsSet()
$link->setRoute('api/docs');
$this->assertTrue($link->isComplete());
}

/**
* @group 79
*/
public function testFactoryCanGenerateLinkWithUrl()
{
$rel = 'describedby';
$url = 'http://example.com/docs.html';
$link = Link::factory(array(
'rel' => $rel,
'url' => $url,
));
$this->assertInstanceOf('PhlyRestfully\Link', $link);
$this->assertEquals($rel, $link->getRelation());
$this->assertEquals($url, $link->getUrl());
}

/**
* @group 79
*/
public function testFactoryCanGenerateLinkWithRouteInformation()
{
$rel = 'describedby';
$route = 'api/docs';
$params = array('version' => '1.1');
$options = array('query' => 'version=1.1');
$link = Link::factory(array(
'rel' => $rel,
'route' => array(
'name' => $route,
'params' => $params,
'options' => $options,
),
));

$this->assertInstanceOf('PhlyRestfully\Link', $link);
$this->assertEquals('describedby', $link->getRelation());
$this->assertEquals($route, $link->getRoute());
$this->assertEquals($params, $link->getRouteParams());
$this->assertEquals($options, $link->getRouteOptions());
}
}

0 comments on commit 189b471

Please sign in to comment.