What would be great is if this module could provide deserialization of the incoming request content for POST/PATCH requests and then invoke the controller.

To deserialize, we need a resource type, and the deserialization class, and optional relationship key. If the relationship key is missing, our deserialization class must be JsonApiDocumentTopLevel. Otherwise it is ResourceIdentifier.

A route can provide defaults to define the resource type to be used. This ends up matching Routes::RESOURCE_TYPE_KEY. This isn't quite perfect.

In the Cart API we use the following to support a resource type against the main entity type and no bundle field considerations.

$resource_type = new ResourceType('commerce_order', 'virtual', EntityInterface::class);

This is a custom resource type to handle PATCHING a relationship without knowing the bundle.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mglaman created an issue. See original summary.

gabesullice’s picture

Issue tags: +API-First Initiative
FileSize
35.41 KB

I still have to do a review of my own work... and probably clean up a ton (I've been writing code for two days and haven't done any cleanup), but I finally got POST working in a way that I'm really quite happy with. Implementing resources which create a custom resource are as simple as:

  /**
   * Process the resource request.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request.
   * @param \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel $document
   *   The deserialized request document.
   *
   * @return \Drupal\jsonapi\ResourceResponse
   *   The response.
   *
   * @throws
   */
  public function process(Request $request, JsonApiDocumentTopLevel $document): ResourceResponse {
    return $this->processEntityCreation($request, $document);
  }

    /**
   * Modifies the created entity before it is saved.
   *
   * This method can be overridden so that custom JSON:API resources can modify
   * the created entity before it is saved.
   *
   * Classes overriding this method have access to the same arguments as a
   * JSON:API resource's process() method by calling
   * $request->arguments->get($arg_name).
   *
   * @param \Drupal\Core\Entity\EntityInterface $created_entity
   *   The created entity.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   */
  protected function modifyCreatedEntity(EntityInterface $created_entity, Request $request) {
    // For example...
    // assert($created_entity instanceof NodeInterface);
    // $created_entity->setOwnerId($this->currentUser->id());
  }

In the above,

  public function process(Request $request, JsonApiDocumentTopLevel $document): ResourceResponse {
    return $this->processEntityCreation($request, $document);
  }

JsonApiDocumentTopLevel $document is deserialized by an argument resolver, which means any resource processor can add it, just like a $request, to have it automatically passed in as an argument. I chose not to resolve directly to an entity because the processor might want to access the meta member for example. Additionally, this means that the initial deserialization is not entity-dependent.

$this->processEntityCreation($request, $document) takes care of extracting the primary data from the deserialized document and creating an entity. It takes care of extra boilerplate from the spec like setting the Location header on 201 Created responses.

The most WTF'y thing is probably the Unstable namespace. It's pretty much what it sounds like and I'd like to move more things into it. The idea is to make it very obvious what is an internal implementation detail and what is not, guiding implementers of custom resources to use the methods on the resource base classes that we provide instead of trying to do complicated things we're not yet sure about.

gabesullice’s picture

FileSize
17.8 KB
30.91 KB

Here's a self-review. Attached patch does most of this.


  1. +++ b/jsonapi_resources.services.yml
    @@ -1,7 +1,20 @@
    +  # Enhances http_kernel.controller.argument_resolver with the ability to resolve request payloads into a JSON:API
    

    exceeds line length limits

  2. +++ b/src/Controller/ArgumentResolver/DocumentResolver.php
    @@ -0,0 +1,68 @@
    +    $decoded = Json::decode((string) $request->getContent());
    

    This needs error handling.

  3. +++ b/src/Controller/ArgumentResolver/DocumentResolver.php
    @@ -0,0 +1,68 @@
    +      if (!empty(array_intersect_key($decoded['data'][0], array_flip(['attributes', 'relationships'])))) {
    +        throw new UnprocessableEntityHttpException("To add or update a resource object, the request document's primary data must not be an array.");
    +      }
    +      else {
    +        throw new HttpException(501, 'The JSON:API Resources module does not yet support updating relationships.');
    

    Needs a comment describing how this condition works.

  4. +++ b/src/Controller/JsonapiResourceController.php
    @@ -80,6 +91,7 @@ final class JsonapiResourceController {
         $resource = $this->classResolver->getInstanceFromDefinition($definition);
         assert($resource instanceof JsonapiResourceInterface);
    +    $this->resourceDependencyInjector->injectResourceDependencies($resource);
    

    Instead of doing this in the controller, we could decorate the class resolver.

  5. +++ b/src/Resource/EntityResourceBase.php
    @@ -7,11 +7,19 @@ use Drupal\Core\Entity\EntityTypeManagerInterface;
    +  use EntityCreationTrait {
    +    processEntityCreation as unstableProcessEntityCreationDoNotUse;
    +    modifyCreatedEntity as unstableModifyCreatedEntityDoNotUse;
    +  }
    

    These method name overrides are unused/outdated.

  6. +++ b/src/Resource/EntityResourceBase.php
    @@ -27,6 +35,13 @@ abstract class EntityResourceBase extends ResourceBase {
    +  /**
    +   * The resource object to entity field value mapper.
    +   *
    +   * @var \Drupal\jsonapi_resources\Unstable\ResourceObjectToEntityMapper
    +   */
    +  private $resourceObjectToEntityMapper;
    +
    

    This can be removed. This property is part of the included trait.

  7. +++ b/src/Resource/EntityResourceBase.php
    @@ -104,4 +119,19 @@ abstract class EntityResourceBase extends ResourceBase {
    +   * @param $entity_type_id
    

    Needs a description.

  8. +++ b/src/Unstable/NewResourceObject.php
    @@ -0,0 +1,36 @@
    +final class NewResourceObject extends ResourceObject {
    

    Needs a class comment.

  9. +++ b/src/Unstable/NewResourceObject.php
    @@ -0,0 +1,36 @@
    +  /**
    +   * {@inheritdoc}
    +   *
    +   * Overridden to make the constructor protected.
    +   */
    +  protected function __construct(CacheableDependencyInterface $cacheability, ResourceType $resource_type, $id, $revision_id, array $fields, LinkCollection $links) {
    

    I think that this isn't necessary.

  10. +++ b/src/Unstable/NewResourceObject.php
    @@ -0,0 +1,36 @@
    +  /**
    +   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
    +   * @param array $primary_data
    +   *
    +   * @return \Drupal\jsonapi_resources\Unstable\NewResourceObject
    +   */
    

    Needs proper docblock.

  11. +++ b/src/Unstable/ResourceObjectToEntityMapper.php
    @@ -0,0 +1,231 @@
    +/**
    + * Service which maps a resource object into an entity.
    + *
    + * @internal
    + *   Do not use this class. It is for internal use and will be phased out when
    + *   core support for similar behavior exists.
    + */
    

    Explain that is mostly a replication of JSON:API's ContentEntityDenormalizer.

  12. +++ b/src/Unstable/ResourceObjectToEntityMapper.php
    @@ -0,0 +1,231 @@
    +    elseif (in_array(ConfigEntityInterface::class, class_implements($target_class, TRUE) ?: [])) {
    +      return $this->mapResourceObjectToConfigEntityValues($resource_object);
    +    }
    

    I think we should actually omit config entity support.

  13. +++ b/src/Unstable/ResourceObjectToEntityMapper.php
    @@ -0,0 +1,231 @@
    +  protected function mapResourceObjectToContentEntityValues(ResourceObject $resource_object): array {
    

    This is silly. This method can just call $this->serializer->denormalizer() to reuse JSON:API's denormalizer. It doesn't need to copy its code.

  14. +++ b/src/Unstable/ResourceObjectToEntityMapperAwareInterface.php
    @@ -0,0 +1,9 @@
    +interface ResourceObjectToEntityMapperAwareInterface {
    

    Needs a docblock.

  15. +++ b/src/Unstable/ResourceObjectToEntityMapperAwareTrait.php
    @@ -0,0 +1,29 @@
    + * Trait ResourceObjectToEntityMapperAwareTrait
    

    Needs a better comment.

  16. +++ b/tests/modules/jsonapi_resources_test/jsonapi_resources_test.routing.yml
    @@ -11,6 +11,21 @@ jsonapi_resources_test.author_content:
    +  options:
    +    parameters:
    +      user:
    +        type: entity:user
    

    We can eliminate this if we set our resource enhancer to a lower priority. I'll open a separate issue for that.

mglaman’s picture

THIS LOOKS SO COOL.

  1. +++ b/src/Controller/ArgumentResolver/DocumentResolver.php
    @@ -0,0 +1,72 @@
    +      else {
    +        throw new HttpException(501, 'The JSON:API Resources module does not yet support updating relationships.');
    +      }
    

    😬I'll have to lend a hand here, as Cart API is solely this.

    I think it's fine as a follow up once we land POST/PATCH/DELETE for individuals

  2. +++ b/src/Controller/ArgumentResolver/DocumentResolver.php
    @@ -0,0 +1,72 @@
    +    yield new JsonApiDocumentTopLevel(new ResourceObjectData([$resource_object], 1), new NullIncludedData(), new LinkCollection([]));
    

    ooo gettin' fancy with yield

    Does yield benefit us over return? this isn't an iterator (I'm curious)

  3. +++ b/src/Unstable/EntityCreationTrait.php
    @@ -0,0 +1,81 @@
    +    $entity = $this->resourceObjectToEntityMapper->createEntityFromResourceObject($data->getIterator()->current());
    +
    +    // Allow the class using this trait to modfiy the created entity before it
    +    // is saved.
    +    $this->modifyCreatedEntity($entity, $request);
    +    $entity->save();
    

    Is this validated? Is it prior in createEntityFromResourceObject?

    Either way we should after the resource modified it

  4. +++ b/tests/modules/jsonapi_resources_test/src/Resource/FeaturedNodes.php
    @@ -44,12 +44,7 @@ class FeaturedNodes extends EntityQueryResourceBase {
    -    $entity_type = $this->entityTypeManager->getDefinition('node');
    -    $bundle_storage = $this->entityTypeManager->getStorage($entity_type->getBundleEntityType());
    -    $bundles = array_values($bundle_storage->getQuery()->execute());
    -    return array_map(function ($bundle) {
    -      return $this->resourceTypeRepository->get('node', $bundle);
    -    }, $bundles);
    +    return $this->getResourceTypesByEntityTypeId('node');
    

    👍

gabesullice’s picture

#4:

1. Yeah... I would love your help thinking that feature through. We need some way to indicate that a route is a relationship route. I couldn't think of an elegant way to do that yet.
2. This is just me copying code 🤗 I don't know if yield is important or not.
3. ohhh, excellent catch. It is not validated. 😬
4. :)

gabesullice’s picture

Status: Active » Needs review
FileSize
7.14 KB
33.88 KB

This addresses #4.3 and sorta starts 4.1. Also addresses #3.3.

gabesullice’s picture

Title: Provide deserialization for POST/PATCH endpoints » Provide API for easily creating resources for adding new entities

Descoping this issue because:

1. I would like to handle relationships in a separate issue.

2. I started to add support for entity PATCH requests but then I realized, that would be a pointless feature. Why? Because when you create a node--reminder resource by POSTing to /jsonapi/user/{user}/reminders, you get back a resource with a Location: /jsonapi/node/reminder/{id} header and a self link on the resource object with the same path. IOW, even if you have custom collections and custom endpoint to create entities, those entities already have a unique URL for updating and deleting them! It would be silly to invest a ton of effort into making custom routes for updating them.


Therefore, I think this is ready for a final review by @mglaman.

gabesullice’s picture

Title: Provide API for easily creating resources for adding new entities » Provide API for easily creating resources that add new entities
mglaman’s picture

Assigned: Unassigned » mglaman

#7.1 - agreee

#7.2 double agree

will review tomorrow

mglaman’s picture

Assigned: mglaman » Unassigned

I think this looks great. I had one main piece of feedback, for extractResourceObjectFromRequest. I ran Cart API tests against the patch and had no breakage. Once this lands I may try to experiment with resolving a JSON API document from PATCH.

One reason it would be beneficial to have a custom PATCH route: permissions. You may not want to grant a user permissions to edit X entity type and bundles and then hope there is EntityOwnerInterface and Entity API access control. We have this for Order Items - we don't want you to just edit any order item. Only the one that belongs to the order it is attached to, and that you own the order. Right now it's easier as a custom resource until work is spent moving down into the entity access system. (also, just better DX control). But I went off the rails a bit here.

  1. +++ b/src/Controller/ArgumentResolver/DocumentResolver.php
    @@ -0,0 +1,142 @@
    +    yield new JsonApiDocumentTopLevel(new ResourceObjectData([$this->extractResourceObjectFromRequest($request)], 1), new NullIncludedData(), new LinkCollection([]));
    ...
    +  protected function extractResourceObjectFromRequest(Request $request) {
    

    I know we're not supporting > 1 cardinality to start, it would be probably a good idea to have this called

    extractResourceObjectsFromRequest. That way it returns an array and we can know the request cardinality.

    This is just a future enhancement for when we support relationships.

  2. +++ b/src/Resource/EntityResourceBase.php
    @@ -7,11 +7,16 @@ use Drupal\Core\Entity\EntityTypeManagerInterface;
    +abstract class EntityResourceBase extends ResourceBase implements ResourceObjectToEntityMapperAwareInterface {
    +
    +  use EntityCreationTrait;
    

    Should EntityResourceBase implement this by default? Or pick and choose per implementation, and documented.

    I guess it does no harm since you must explicitly invoke

    return $this->processEntityCreation($request, $document);
    
  3. +++ b/src/Unstable/EntityCreationTrait.php
    @@ -0,0 +1,95 @@
    +trait EntityCreationTrait {
    +
    +  use ResourceObjectToEntityMapperAwareTrait;
    

    I was trying to track down implementations of ResourceObjectToEntityMapperAwareInterface and had no idea where the implementation existed.

    Just fields weird having a trait use another trait to implement an interface. But it makes sense since this requires that property.

gabesullice’s picture

Whoa, I totally missed this review @mglaman! Thanks.

#10:

1. The reason that this is not plural is because the spec does not permit request documents with an array of resource objects.
2. That was my conclusion too :)
3. Honestly, I think we could get rid of ResourceObjectToEntityMapperAwareTrait entirely by merging it up into EntityCreationTrait. The lower level trait was created in anticipation of supporting entity PATCH requests, however, since #7.2, that feature has been dropped.

That said, your comment in #10 suggests that maybe we should not drop that feature. So, because we don't yet know what we want, I think it makes sense to keep the trait in this patch. It lives in the Unstable namespace, so we can always drop it later.

gabesullice’s picture

Status: Needs review » Reviewed & tested by the community
gabesullice Today at 12:12 PM
@mglaman :table_tennis_paddle_and_ball:  https://www.drupal.org/project/jsonapi_resources/issues/3088477#comment-13340617

gabesullice  1 hour ago
I'd like to commit this and then move to documentation and some cleanup for the beta

mglaman:centarro:  1 hour ago
reading quick

mglaman:centarro:  1 hour ago
looks good to commit

  • gabesullice committed b17b049 on 8.x-1.x
    Issue #3088477 by gabesullice, mglaman: Provide API for easily creating...
gabesullice’s picture

Status: Reviewed & tested by the community » Fixed

Status: Fixed » Closed (fixed)

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