laminas-api-tools / api-tools-hal

Laminas Module providing Hypermedia Application Language assets and rendering
https://api-tools.getlaminas.org/documentation
BSD 3-Clause "New" or "Revised" License
6 stars 12 forks source link

Hinting of embedded resources #16

Open weierophinney opened 4 years ago

weierophinney commented 4 years ago

I've been struggling with a problem for some time regarding how relationship are handled on entities, specifically when embedded into a resource. The problem (and I assume it is a fairly common one) is how to provide relationships in a sane and dependable way without returning the entire (or most of the) database in embedded resources (and dealing with looping recursion problems) or always needing to fetch relationships. HAL spec is a bit fuzzy on how _embedded resources are supplied which makes it harder to create a dependable abstract consumers. For example, HAL Clients don't know if the non-rendered entity is actually rendered or not (or is just a link) or whether it is a full or partial rendering. The first case is an easier problem to solve abstractly and a few suggestions are below.

For the sake of example lets assume the following:

We can either:

  1. Depend on clients to inspect the payload and if the only property is the _links object, the client should understand to fetch that resource also when needed. This is how it is currently working
{
    "id": "123",
    "name": "Jane Doe",
   "gender": "female",
    "_embedded": {
        "bestFriend": {
            "_links": {
                "self": {
                    "href": "http://abc.tld/api/person/456"
                }
            }
        }
    },
    "_links": {
        "self": {
            "href": "http://abc.tld/api/person/123"
        }
    }
}
  1. Providing a 'profile' parameter which can be used as a hint Link Only:
{
    "id": "123",
    "name": "Jane Doe",
    "gender": "female",
    "_embedded": {
        "bestFriend": {
            "_profile": "link",
            "_links": {
                "self": {
                    "href": "http://abc.tld/api/person/456"
                }
            }
        }
    },
    "_links": {
        "self": {
            "href": "http://abc.tld/api/person/123"
        }
    }
}

Partial Resource:

{
    "id": "123",
    "name": "Jane Doe",
    "gender": "female",
    "_embedded": {
        "bestFriend": {
            "name": "John Smith",
            "_profile": "partial",
            "_links": {
                "self": {
                    "href": "http://abc.tld/api/person/456"
                }
            }
        }
    },
    "_links": {
        "self": {
            "href": "http://abc.tld/api/person/123"
        }
    }
}
  1. Provide the embedded entities as _links on the resource instead of embedding the entity that contains only the link (removes any ambiguity).
{
    "id": "123",
    "name": "Jane Doe",
    "gender": "female",
    "_links": {
        "self": {
            "href": "http://abc.tld/api/person/123"
        },
        "bestFriend": {
            "href": "http: //abc.tld/api/person/456"
        }
    }
}

I've created PR #118 to show an example of how we can enabling linking insteading of embedding and would love to get feedback on how others are solving this kind of issue, and/or if I should focus time on building out one of the above potential solutions.


Originally posted by @nobesnickr at https://github.com/zfcampus/zf-hal/issues/118

weierophinney commented 4 years ago

In our project we do exactly like you propose in your last solution. Resources that are not embedded are linked. This seems to work very well in practice. We also took this a step further. Resources that are already rendered once in the response will not be rendered again in another resource in the same response.

For example. We render a resource collection. The first resource in the collection has a resource A embedded. The second resource has the same resource A embedded. Instead of embedding it again in the second resource it will be linked.

Right now we solve this with some customization of the Hal plugin. We add a hash for each entity that is rendered into an array and while rendering we check every time whether the object with this hash was already once rendered in the same response with a isRendered method (check if the hash is in the array). If it was rendered we only add a link and remove the element from the array that is in the process of being rendered.

We can do this because on the client side we cache the json objects by their respective self link. During parsing of the json response on the client all links with the same self-href are automatically exchanged with the fully rendered instance in the client sided object model. This means that we need the full json object only once in our response, all other instances can simply be linked since the client will automatically exchange those with the full object during parsing.

This delivers a performance increase since on the server we only need to render each entity once and our json response object is much more compact since we don't duplicate any json data.

This is implementation is according to the caching specs in hal section 8.3.

We first tried to solve this in the EntityExtractor added to the project which almost does the same (map of hashes in $serializedEntities and checking whether a $entity is serialized (is rendered) in the extract method like this:

if (isset($this->serializedEntities[$entity])) {
    return $this->serializedEntities[$entity];
}

Which virtually comes down to our isRendered method.

I would be interested in implementing such solution this part of the library.


Originally posted by @Wilt at https://github.com/zfcampus/zf-hal/issues/118#issuecomment-136306852

weierophinney commented 4 years ago

@nobesnickr Hi, sorry to disturb you, i would like to ask you a question about how you nested the entities. can you please give a detailed explanation on how to configure the 2 classes to get this: { "id": "123", "name": "Jane Doe", "gender": "female", "_embedded": { "bestFriend": { "_profile": "link", "_links": { "self": { "href": "http://abc.tld/api/person/456" } } } }, "_links": { "self": { "href": "http://abc.tld/api/person/123" } } }
on apigility?. Thank you


Originally posted by @LeeRoi at https://github.com/zfcampus/zf-hal/issues/118#issuecomment-299460891