Skip to content

Commit

Permalink
Merge branch 'feature/force-relation-collections'
Browse files Browse the repository at this point in the history
Close #4
  • Loading branch information
weierophinney committed Aug 7, 2017
2 parents eaf339a + abfe834 commit 2d211e7
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 6 deletions.
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,39 @@

All notable changes to this project will be documented in this file, in reverse chronological order by release.

## 0.3.0 - TBD

### Added

- [#4](https://github.com/weierophinney/hal/pull/4) adds the ability to force
both links and embedded resources to be rendered as collections, even if the
given relation only contains one item.

To force a link to be rendered as a collection, pass the attribute
`__FORCE__COLLECTION__` with a boolean value of `true` (or use the constant
`Link::AS_COLLECTION` to refer to the attribute name).

To force an embedded resource to be rendered as a collection, pass a boolean
`true` as the third argument to `embed()`. Alternately, pass an array
containing the single resource to any of the constructor, `withElement()`, or
`embed()`.

### Changed

- Nothing.

### Deprecated

- Nothing.

### Removed

- Nothing.

### Fixed

- Nothing.

## 0.2.0 - 2017-07-13

### Added
Expand Down
79 changes: 79 additions & 0 deletions doc/book/representations.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,82 @@ $response = $factory->createResponse(

_Do not_ pass the format (e.g., `+json`, `+xml`) when doing so; the factory will
append the appropriate one based on content negotiation.

## Forcing collections for relations

HAL allows links and embedded resources to be represented as:

- a single object
- an array of objects of the same type

Internally, this package checks to see if only one of the item exists, and, if
so, it will render it by itself. However, there are times you may want to force
an array representation. As an example, if your resource models a car, and you
have a `wheels` relation, it would not make sense to return a single wheel, even
if that's all the car currently has associated with it.

To accommodate this, we provide two features.

For links, you may pass a special attribute, `Hal\Link::AS_COLLECTION`, with a
boolean value of `true`; when encountered, this will then be rendered as an
array of links, even if only one link for that relation is present.

```php
$link = new Link(
'wheels',
'/api/car/XXXX-YYYY-ZZZZ/wheels/111',
false,
[Link::AS_COLLECTION => true]
);

$resource = $resource->withLink($link);
```

In the above, you will then get the following within your representation:

```json
"_links": {
"wheels": [
{"href": "/api/car/XXXX-YYYY-ZZZZ/wheels/111"}
]
}
```

To force an embedded resource to be rendered within an array, you have two
options.

First, and simplest, pass the resource within an array when calling
`withElement()`, `embed()`, or passing data to the constructor:

```php
// Constructor:
$resource = new HalResource(['wheels' => [$wheel]]);

// withElement():
$resource = $resource->withElement('wheels', [$wheel]);

// embed():
$resource = $resource->embed('wheels', [$wheel]);
```

Alternately, you can call the `HalResource::embed` method with only the
resource, passing the method a third argument, a flag indicating whether or not
to force an array:

```php
$resource = $resource->embed('wheels', $wheel, true);
```

In each of these cases, assuming no other wheels were provided to the final
resource, you might get a representation such as the following:

```json
"_embedded": {
"wheels": [
{
"_links" => {"self": {"href": "..."}}
"id": "..."
},
]
}
```
42 changes: 36 additions & 6 deletions src/HalResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,12 @@ public function withElements(array $elements) : HalResource
/**
* @param string $name
* @param Resource|Resource[] $resource
* @param bool $forceCollection Whether or not a single resource or an
* array containing a single resource should be represented as an array of
* resources during representation.
* @return Resource
*/
public function embed(string $name, $resource) : HalResource
public function embed(string $name, $resource, bool $forceCollection = false) : HalResource
{
$this->validateElementName($name, __METHOD__);
$this->detectCollisionWithData($name, __METHOD__);
Expand All @@ -200,7 +203,7 @@ public function embed(string $name, $resource) : HalResource
));
}
$new = clone $this;
$new->embedded[$name] = $this->aggregateEmbeddedResource($name, $resource, __METHOD__);
$new->embedded[$name] = $this->aggregateEmbeddedResource($name, $resource, __METHOD__, $forceCollection);
return $new;
}

Expand Down Expand Up @@ -284,10 +287,10 @@ private function detectCollisionWithEmbeddedResource(string $name, string $conte
*
* @return Resource|Resource[]
*/
private function aggregateEmbeddedResource(string $name, $resource, string $context)
private function aggregateEmbeddedResource(string $name, $resource, string $context, bool $forceCollection)
{
if (! isset($this->embedded[$name])) {
return $resource;
return $forceCollection ? [$resource] : $resource;
}

// $resource is an collection; existing individual or collection resource exists
Expand Down Expand Up @@ -373,17 +376,44 @@ private function serializeLinks()

$linkRels = $link->getRels();
array_walk($linkRels, function ($rel) use (&$byRelation, $representation) {
$forceCollection = array_key_exists(Link::AS_COLLECTION, $representation)
? (bool) $representation[Link::AS_COLLECTION]
: false;
unset($representation[Link::AS_COLLECTION]);

if (isset($byRelation[$rel])) {
$byRelation[$rel][] = $representation;
return;
} else {
$byRelation[$rel] = [$representation];
}

// If we're forcing a collection, and the current relation only
// has one item, mark the relation to force a collection
if (1 === count($byRelation[$rel]) && $forceCollection) {
$byRelation[$rel][Link::AS_COLLECTION] = true;
}

// If we have more than one link for the relation, and the
// marker for forcing a collection is present, remove the
// marker; it's redundant. Check for a count greater than 2,
// as the marker itself will affect the count!
if (2 < count($byRelation[$rel]) && isset($byRelation[$rel][Link::AS_COLLECTION])) {
unset($byRelation[$rel][Link::AS_COLLECTION]);
}
$byRelation[$rel] = [$representation];
});

return $byRelation;
}, []);

array_walk($relations, function ($links, $key) use (&$relations) {
if (isset($relations[$key][Link::AS_COLLECTION])) {
// If forcing a collection, do nothing to the links, but DO
// remove the marker indicating a collection should be
// returned.
unset($relations[$key][Link::AS_COLLECTION]);
return;
}

$relations[$key] = 1 === count($links) ? array_shift($links) : $links;
});

Expand Down
2 changes: 2 additions & 0 deletions src/Link.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

class Link implements EvolvableLinkInterface
{
const AS_COLLECTION = '__FORCE_COLLECTION__';

/**
* @var array
*/
Expand Down
50 changes: 50 additions & 0 deletions test/HalResourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -498,4 +498,54 @@ public function testJsonSerializeReturnsHalDataStructure(HalResource $resource,
{
$this->assertEquals($expected, $resource->jsonSerialize());
}

public function testAllowsForcingResourceToAggregateAsACollection()
{
$resource = (new HalResource())
->withLink(new Link('self', '/api/foo'))
->embed(
'bar',
new HalResource(['bar' => 'baz'], [new Link('self', '/api/bar')]),
true
);

$expected = [
'_links' => [
'self' => [
'href' => '/api/foo',
],
],
'_embedded' => [
'bar' => [
[
'bar' => 'baz',
'_links' => [
'self' => ['href' => '/api/bar'],
],
],
],
],
];

$this->assertEquals($expected, $resource->toArray());
}

public function testAllowsForcingLinkToAggregateAsACollection()
{
$link = new Link('foo', '/api/foo', false, [Link::AS_COLLECTION => true]);
$resource = new HalResource(['id' => 'foo'], [$link]);

$expected = [
'_links' => [
'foo' => [
[
'href' => '/api/foo',
],
],
],
'id' => 'foo',
];

$this->assertEquals($expected, $resource->toArray());
}
}

0 comments on commit 2d211e7

Please sign in to comment.