phly / PhlyRestfully

ZF2 module for creating RESTful JSON APIs using HAL and API-Problem
108 stars 45 forks source link

Use metadata map when rendering collections of "related" resources #102

Open ivan-novakov opened 11 years ago

ivan-novakov commented 11 years ago

Suppose we have users and groups resources with the corresponding routes - /users[/:id] and /groups[/:id].

If I need to list users which are members of a group I have to use a "child" resource with a route /groups/:id/users, which returns a collection of users.

I followed the documentation and everything worked fine. Except one thing - the self links of the users under /groups/:id/users were (as expected) using the route from the corresponding resource, for example:

https://server/groups/123/users/456

Instead, I'd like to have the "standard" location:

https://server/users/456

I tried to define a route option for the Users collection in the metadata map, but it was ignored. I examined the extractCollection() method in Plugin\HalLinks.php and it seems that metadata are taken into consideration only if the resource is embedded, but not if it is in the "first level" of the collection.

I may provide a solution, but I just wanted to ask, if I'm getting this right.

Wilt commented 11 years ago

I am not totally sure if i understand it correctly, but can you not pass the correct route/url to the collection in the hydrator (where you embed it in your resource). So when you add the users collection to your group/:id object (in the hydrator) pass a route variable that points to https://server/users/456. Hope that helps

ivan-novakov commented 11 years ago

My case is very similar to the one explained in the docs - Advanced routing. The GET /groups/:id/users request is routed to the fetchAll method of the GroupUserResourceController, so the expected response is a collection:

{
"_links": {
    "self": {
        "href": "https://server.org/groups/456/users"
    }
},
"_embedded": {
    "users": [
        {
            "id": 123,
            "name": "Foo Bar",
            "_links": {
                "self": {
                    "href": "https://server.org/groups/456/users/123"
                }
            }
        },
    ]
}
}

The difference from the example in the docs is that the Users resource is a standalone resource, so I'd like to have the individual collection items' links like https://server.org/users/123 instead of https://server.org/groups/456/users/123.

In such case the the route for the items in the collection is taken from the controller config (in `$config['phlyrestfully']['resources']) and the metadata are ignored, which seems to me a bit incosistent.

Of course, there are several possible workarounds and also - it's possible that I'm doing something wrong.

Wilt commented 11 years ago

Maybe you just have to attach a listener to the createLink event for this particular route /groups/users like described here: https://phlyrestfully.readthedocs.org/en/latest/ref/advanced-routing.html Like that you can redefine the _links self href? Is that the solution you are looking for?

ivan-novakov commented 11 years ago

Yes, this might be a solution, but I thought it could be done in a more elegant way :). The page you are referencing itself says:

"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."

Thanks for your help anyway.

Wilt commented 11 years ago

What goes wrong when you use metadata mapping...? Maybe something is wrong in your mapping definitions? Check whether the classname of the resource you embed corresponds to the classname you use for mapping. I have some similar issues. If the classnames don't match exactly the rendering of the hal resource fails.

ivan-novakov commented 11 years ago

Ok, I'll try to be more specific. I have these routes:

        'users' => array(
            'type' => 'Segment',
            'options' => array(
                'route' => '/users[/:user_id]',
                'defaults' => array(
                    'controller' => 'PerunWs\UserController'
                )
            ),
        ),

        'groups' => array(
            'type' => 'Segment',
            'options' => array(
                'route' => '/groups[/:group_id]',
                'defaults' => array(
                    'controller' => 'PerunWs\GroupController'
                )
            ),
            'may_terminate' => true,

            'child_routes' => array(
                'group-users' => array(
                    'type' => 'Segment',
                    'options' => array(
                        'route' => '/users[/:user_id]',
                        'defaults' => array(
                            'controller' => 'PerunWs\GroupUsersController'
                        )
                    )
                )
            )
        )

And these resource controllers:

        'PerunWs\UserController' => array(
            'identifier_name' => 'user_id',
            'listener' => 'PerunWs\UserListener',
            'resource_identifiers' => array(
                'UserResource'
            ),
            'collection_http_options' => array(
                'get'
            ),
            'collection_name' => 'users',
            'page_size' => 10,
            'resource_http_options' => array(
                'get'
            ),
            'route_name' => 'users'
        ),

        'PerunWs\GroupController' => array(
            'identifier_name' => 'group_id',
            'listener' => 'PerunWs\GroupsListener',
            'resource_identifiers' => array(
                'GroupsResource'
            ),
            'collection_http_options' => array(
                'get',
                'post'
            ),
            'collection_name' => 'groups',
            'page_size' => 10,
            'resource_http_options' => array(
                'get',
                'patch',
                'delete'
            ),
            'route_name' => 'groups'
        ),

        'PerunWs\GroupUsersController' => array(
            'identifier_name' => 'user_id',
            'listener' => 'PerunWs\GroupUsersListener',
            'resource_identifiers' => array(
                'GroupUsersResource'
            ),
            'collection_http_options' => array(
                'get'
            ),
            'collection_name' => 'users',
            'page_size' => 10,
            'resource_http_options' => array(
                'put',
                'delete'
            ),
            'route_name' => 'groups/group-users'
        )

Now, the GET /groups/123/users request is routed to PerunWs\GroupUsersController, action getList(). My persistance layer returns a list of users and the resource controller creates a HalCollection and uses the route configured for the controller - groups/group-users:

https://github.com/phly/PhlyRestfully/blob/master/src/PhlyRestfully/ResourceController.php#L490

This route is used for "self" link generation, which in this case is:

https://server.org/groups/123/users

The problem is that the items of the collection use the same route for generating "self" links, although there exists a valid mapping in the metadata map.

Here it is obvious, that the route name assigned to the HalCollection earlier is used in the "self" link generation of the items:

https://github.com/phly/PhlyRestfully/blob/master/src/PhlyRestfully/Plugin/HalLinks.php#L784 https://github.com/phly/PhlyRestfully/blob/master/src/PhlyRestfully/Plugin/HalLinks.php#L793 https://github.com/phly/PhlyRestfully/blob/master/src/PhlyRestfully/Plugin/HalLinks.php#L839

The metadata map is used only, if an item of the collection contains an embedded resource:

https://github.com/phly/PhlyRestfully/blob/master/src/PhlyRestfully/Plugin/HalLinks.php#L811

It is not used for the "first-level" item itself. And the metadata mapping is OK, because the right hydrator is used and the item is converted to an array properly:

https://github.com/phly/PhlyRestfully/blob/master/src/PhlyRestfully/Plugin/HalLinks.php#L807

The only thing needed is to get the route name from the metadata map along with the right hydrator. Maybe I could prepare a patch to make it more clear.

Wilt commented 11 years ago

That's helpful, but please also show me your resource mapping from your config.php :-)

ivan-novakov commented 11 years ago

Sure, actually it's here on github :). It's the InoPerunApi\Entity\Collection\RichMemberCollection which contains items of type InoPerunApi\Entity\RichMember:

https://github.com/ivan-novakov/php-perun-ws/blob/master/module/PerunWs/config/module.config.php#L166