Advanced Routing¶
The recommended route for a resource is a Zend\Mvc\Router\Http\Segment
route, with an identifier:
'route' => '/resource[/:id]'
This works great for standalone resources, but poses a problem for hierarchical resources. As an example, if you had a “users” resource, but then had “addresses” that were managed as part of the user, the following route definition poses a problem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 'users' => array(
'type' => 'Segment',
'options' => array(
'route' => '/users[/:id]',
'controller' => 'UserResourceController',
),
'may_terminate' => true,
'child_routes' => array(
'addresses' => array(
'type' => 'Segment',
'options' => array(
'route' => '/addresses[/:id]',
'controller' => 'UserAddressResourceController',
),
),
),
),
|
Spot the problem? Both the parent and child have an “id” segment, which means there is a conflict. Let’s refactor this a bit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 'users' => array(
'type' => 'Segment',
'options' => array(
'route' => '/users[/:user_id]',
'controller' => 'UserResourceController',
),
'may_terminate' => true,
'child_routes' => array(
'type' => 'Segment',
'options' => array(
'route' => '/addresses[/:address_id]',
'controller' => 'UserAddressResourceController',
),
),
),
|
Now we have a new problem, or rather, two new problems: by default, the
ResourceController
uses “id” as the identifier, and this same identifier
name is used to generate URIs. How can we change that?
First, the ResourceController
allows you to define the identifier name for
the specific resource being exposed. You can do this via the
setIdentifierName()
method, but more commonly, you’ll handle it via the
identifier_name
configuration parameter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 'phlyrestfully' => array(
'resources' => array(
'UserResourceController' => array(
// ...
'identifier_name' => 'user_id',
// ...
),
'UserAddressResourceController' => array(
// ...
'identifier_name' => 'address_id',
// ...
),
),
),
|
If you are rendering child resources as part of a resource, however, you need to hint to the renderer about where to look for an identifier.
There are several mechanisms for this: the getIdFromResource
and
createLink
events of the PhlyRestfully\Plugin\HalLinks
plugin; or
a metadata map.
The HalLinks
events are as followed, and triggered by the methods specified:
Event name | Method triggering event | Parameters |
---|---|---|
createLink | createLink |
|
getIdFromResource | getIdFromResource |
|
Let’s dive into each of the specific events.
Note
In general, you shouldn’t need to tie into the events listed on this page very often. The recommended way to customize URL generation for resources is to instead use a metadata map.
createLink event¶
The createLink
method is currently called only from
PhlyRestfully\ResourceController::create()
, and is used to generate the
Location
header. Essentially, what it does is call the url()
helper with
the passed route, and the serverUrl()
helper with that result to generate a
fully-qualified URL.
If passed a resource identifier and resource, you can attach to the event the method triggers in order to modifiy the route parameters and/or options when generating the link.
Consider the following scenario: you need to specify an alternate routing parameter to use for the identifier, and you want to use the “user” associated with the resource as a route parameter. Finally, you want to change the route used to generate this particular URI.
The following will do that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | $request = $services->get('Request');
$sharedEvents->attach('PhlyRestfully\Plugin\HalLinks', 'createLink', function ($e) use ($request) {
$resource = $e->getParam('resource');
if (!$resource instanceof Paste) {
// only react for a specific type of resource
return;
}
// The parameters here are an ArrayObject, which means we can simply set
// the values on it, and the method calling us will use those.
$params = $e->getParams();
$params['route'] = 'paste/api/by-user';
$id = $e->getParam('id');
$user = $resource->getUser();
$params['params']['paste_id'] = $id;
$params['params']['user_id'] = $user->getId();
}, 100);
|
The above listener will change the route used to “paste/api/by-user”, and ensure that the route parameters “paste_id” and “user_id” are set based on the resource provided.
The above will be called with create
is successful. Additionally, you can
use the HalLinks
plugin from other listeners or your view layer, and call
the createLink()
method manually – which will also trigger any listeners.
getIdFromResource event¶
The getIdFromResource
event is only indirectly related to routing. Its
purpose is to retrieve the identifier for a given resource so that a “self”
relational link may be generated; that is its sole purpose.
The event receives exactly one argument, the resource for which the identifier is needed. A default listener is attached, at priority 1, that uses the following algorithm:
- If the resource is an array, and an “id” key exists, it returns that value.
- If the resource is an object and has a public “id” property, it returns that value.
- If the resource is an object, and has a public
getId()
method, it returns the value returned by that method.
In all other cases, it returns a boolean false
, which generally results in
an exception or other error.
This is where you, the developer come in: you can write a listener for this event in order to return the identifier yourself.
As an example, let’s consider the original example, where we have “user” and “address” resources. If these are of specific types, we could write listeners like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | $sharedEvents->attach('PhlyRestfully\Plugin\HalLinks', 'getIdFromResource', function ($e) {
$resource = $e->getParam('resource');
if (!$resource instanceof User) {
return;
}
return $resource->user_id;
}, 100);
$sharedEvents->attach('PhlyRestfully\Plugin\HalLinks', 'getIdFromResource', function ($e) {
$resource = $e->getParam('resource');
if (!$resource instanceof UserAddress) {
return;
}
return $resource->address_id;
}, 100);
|
Since writing listeners like these gets old quickly, I recommend using a metadata map instead.