Looking at the following code, there is some developer experience improvements that could be done to make this seem less confusing / forcing developers to review examples.

    $data = new ResourceObjectData(array_map(function (NodeInterface $node) {
      $resource_type = $this->resourceTypeRepository->get($node->getEntityTypeId(), $node->bundle());
      return ResourceObject::createFromEntity($resource_type, $node);
    }, $nodes));

It's also missing access checks and other goodies available in JSON API's EntityResource.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mglaman created an issue. See original summary.

mglaman’s picture

Status: Active » Needs review
FileSize
11.35 KB

Here's a patch. The code was reduced to calling one function exposed from the JSON:API EntityResource controller.

    $node_storage = $this->entityTypeManager->getStorage('node');
    $content_query = $node_storage->getQuery()
      ->condition('status', NodeInterface::PUBLISHED)
      ->condition('uid', $user->id());

    $nodes = $this->loadEntityResourceObjectsWithAccess($node_storage, $content_query->execute());
    $data = new ResourceObjectData($nodes);
mglaman’s picture

+++ b/src/Resource/EntityResourceBase.php
@@ -38,8 +50,45 @@ abstract class EntityResourceBase extends ResourceBase {
+  protected function loadEntityResourceObjectsWithAccess(EntityStorageInterface $storage, array $ids, $load_latest_revisions = FALSE): array {
+    return $this->entityResourceShim->loadEntitiesWithAccess($storage, $ids, $load_latest_revisions);

This was renamed from the parent so that it makes more sense.

loadEntitiesWithAccess actually returns resource objects, not entities.

gabesullice’s picture

Assigned: Unassigned » gabesullice
Status: Needs review » Needs work
FileSize
16.99 KB
13.62 KB
  1. +++ b/tests/modules/jsonapi_resources_test/src/Resource/FeaturedNodes.php
    @@ -25,16 +25,12 @@ class FeaturedNodes extends EntityResourceBase {
    +    $nodes = $this->loadEntityResourceObjectsWithAccess($node_storage, $featured_query->execute());
    
    

    I think this exposes too much of the implementation underneath. It seems like a poor DX to pass the entity storage system. It's also a bit heavy handed to enforce access checking IMO. I think this could be a flag that simply defaults to TRUE.

  2. +++ b/src/Controller/jsonapi/EntityResourceShim.php
    @@ -0,0 +1,43 @@
    +final class EntityResourceShim extends EntityResource {
    
    

    I think this was useful in your initial experiments to minimize coupling and complexity, but since this is an experiment for how to provide something that might eventually be in core, I think it's okay to copy a few more of the internals for EntityResource, rather than just rely on them through this shim.


Given that ^

I got rid of the shim and copied a little bit of EntityResource code and directly injected the EntityAccessChecker. I also made the loader method a little nicer DX-wise.

I'm going to keep working on this, I'd like to make the entity query bit simpler too.

mglaman’s picture

+++ b/src/Resource/EntityResourceBase.php
@@ -23,12 +44,16 @@ abstract class EntityResourceBase extends ResourceBase {
+    $this->entityAccessChecker = $entity_access_checker;
+    assert(is_string(static::$entityTypeId) && $this->entityTypeManager->hasDefinition(static::$entityTypeId), 'Extensions of \Drupal\jsonapi_resource\Resource\EntityResourceBase must set the static property $entityTypeId to a valid entity type ID or be declared abstract.');

+++ b/tests/modules/jsonapi_resources_test/src/Resource/AuthorContent.php
@@ -12,6 +11,11 @@ use Symfony\Component\HttpFoundation\Request;
+  protected static $entityTypeId = 'node';

This assumes the EntityResource will only perform storage for a single entity type.

I guess that kind of maps how dataProviders in Restful work? But it feels weird that the resource is dictated by a static property.

gabesullice’s picture

Status: Needs work » Needs review
Issue tags: +API-First Initiative
FileSize
37.1 KB
35.96 KB

#5: agreed, I got rid of that bit.

This adds the ability to add pagination to a custom collection route with minimal effort. It also does a whole bunch of the boilerplate needed to handle cacheability for the page parameter.

Honestly, I'm not sure I'm happy with what I have so far. I like the simplicity of the API for extensions for EntityResourceBase, but I think there's a lot of somewhat magical stuff going on too. Maybe instead of passing OffsetPaginationEntityQueryModifier it should instead be a route option in routing.yml, like _offset_pagination: true.

Curious to see what you think @mglaman!

mglaman’s picture

  • Maybe instead of passing OffsetPaginationEntityQueryModifier it should instead be a route option in routing.yml, like _offset_pagination: true.

    I agree it'd be nice to have pagination just work. My fear is that we may start adding a lot of arbitrary route defaults that need documentation. Especially as I explore adding magicalness to POST/PATCH.

    This is why I thought of originally having plugins. Because it makes this a flag method on the plugin.

  • This is tightly coupled to the entity query. I could see having the pager beneficial for anything implementing a query interface, such as custom Search API queries (I think the Search API query abstraction implements a relatable interface.) This would fix pagination for a custom Solr resource per https://glamanate.com/blog/using-json-api-query-your-search-api-indexes

  • +++ b/jsonapi_resources.services.yml
    --- /dev/null
    +++ b/src/EntityQuery/CallableEntityQueryModifier.php
    
    +++ b/src/Resource/EntityResourceBase.php
    @@ -38,8 +68,184 @@ abstract class EntityResourceBase extends ResourceBase {
    +  private function loadResourceObjectsByEntityIds($entity_type_id, array $ids, $load_latest_revisions = FALSE, $check_access = TRUE): array {
    +    $storage = $this->entityTypeManager->getStorage($entity_type_id);
    ...
    +  protected function createResourceObjectFromEntity(EntityInterface $entity, $check_access = TRUE) {
    +    $resource_objects = $this->createResourceObjectsFromEntities([$entity], $check_access);
    ...
    +  protected function createResourceObjectsFromEntities(array $entities, $check_access = TRUE) {
    +    $resource_objects = [];
    +    foreach ($entities as $entity) {
    ...
    +  private function executeQueryInRenderContext(QueryInterface $query, CacheableMetadata $query_cacheability) {
    +    $context = new RenderContext();
    
    +++ b/tests/modules/jsonapi_resources_test/src/Resource/FeaturedNodes.php
    @@ -24,18 +25,15 @@ class FeaturedNodes extends EntityResourceBase {
    +      CallableEntityQueryModifier::createFromCallable(function (QueryInterface $query) {
    +        $query->condition('status', NodeInterface::PUBLISHED);
    +        $query->condition('promote', NodeInterface::PROMOTED);
    +      }),
    +      OffsetPaginationEntityQueryModifier::createFromRequest($request),
    +    ]);
    

    I love all of these helpers.

  • +++ b/src/EntityQuery/OffsetPaginationEntityQueryModifier.php
    @@ -0,0 +1,61 @@
    +   *   A pagination modifier derived from the request.
    
    +++ b/src/Resource/ResourceBase.php
    @@ -46,4 +48,30 @@ abstract class ResourceBase implements ContainerInjectionInterface {
    +   */
    +  protected function createJsonapiResponse(ResourceObjectData $data, Request $request, $response_code = 200, array $headers = [], LinkCollection $links = NULL, array $meta = []) {
    +    return $this->resourceResponseFactory->create($data, $request, $response_code, $headers, $links, $meta);
    

    And this one!

  • +++ b/src/Resource/EntityResourceBase.php
    @@ -38,8 +68,184 @@ abstract class EntityResourceBase extends ResourceBase {
    +  protected function findEntityResourceObjectData($entity_type_id, $entity_query_modifiers = [], $load_latest_revisions = FALSE, $check_access = TRUE) {
    +    $storage = $this->entityTypeManager->getStorage($entity_type_id);
    +    $query = $storage->getQuery();
    ...
    +    $query_cacheability = new CacheableMetadata();
    +    $results = $this->executeQueryInRenderContext(
    +      $query,
    +      $query_cacheability
    +    );
    

    I'd honestly prefer if I just passed in my own entity query that I crafted ahead of time instead of having to pass in modifiers. And then the pagination is just a boolean setting.

    The DX problem we have is definitely ensuring results are executed in a render context.

    If we accepted a query interface (not just entity) we can accept any incoming query (db, search api, etc).

    It also removes the need for entity query modifiers. Which lowers complexity

mglaman’s picture

Search API would need its own helpers, it doesn't share any of the database query interfaces. That dream is dead :P

mglaman’s picture

Also, my fear is that we're going to add additional complexity without exposing proper tools to help developers build with JSON:API instead of putting them into a specific working mindset as the current module does. That's where I think we can be a little more "dangerous" here in this contrib and provide more loose helpers and good examples.

gabesullice’s picture

FileSize
56.46 KB
43.93 KB

I hear you on the necessity of not being too tightly coupled to entity queries. OTOH, I think most custom collections will still be based on entity queries. I think your examples prove that already. Also, all of the existing pagination/sorting/filtering logic in JSON:API is closely coupled to entity queries.

I think it'd be a gigantic lift to decouple (or rewrite them) to be independent to the entity query system. SO... what does this patch do?

It takes out anything entity query specific from EntityResourceBase puts it in EntityQueryResourceBase.

I took your suggestion and nixxed the "modifier" idea. Now you can get a paginator and "apply" it to your raw entity query. It can also generate pagination links.

I threaded cacheability through more things. That's probably just a good thing to do in general.

More helpers.

A couple tests roughly asserting that pagination works as expected.

gabesullice’s picture

FileSize
2.26 KB
43.61 KB

Fixes a few nits.

IMHO, this is good enough to be committed. We can keep iterating on it ofc.

mglaman’s picture

Status: Needs review » Reviewed & tested by the community

I say we merge this sucker.

  • gabesullice committed f622ba2 on 8.x-1.x
    Issue #3088387 by gabesullice, mglaman: Improve the developer experience...
gabesullice’s picture

Assigned: gabesullice » Unassigned
Status: Reviewed & tested by the community » Fixed

🎉

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.