When creating a new entity from jsonapi data the relationships are not handled:

  public static function createFromPrimaryData(ResourceType $resource_type, array $primary_data) {
    $id = $primary_data['id'] ?? \Drupal::service('uuid')->generate();
    $fields = array_merge(
      $primary_data['attributes'] ?? [],
      // @todo: handle relationships.
      []
    );
    return new static(new CacheableMetadata(), $resource_type, $id, NULL, $fields, new LinkCollection([]));
  }
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

sam711 created an issue. See original summary.

sam711’s picture

Here's my first attempt to solve this which is roughly working for me.

I've basically stolen code from Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer, which I think makes a lot of sense here.

I'm also using the jsonapi.resource_type.repository service, which probably shouldn't, but it was already used by this module :)

One thing that is missing, is the access check on the referenced entities and attribute fields.

I assume the create access check for the primary entity is handled on the route.

sam711’s picture

Fixing coding standards.

Field access is checked in the EntityValidationTrait so ignore that part.

sam711’s picture

Assigned: Unassigned » sam711
Status: Active » Needs review
sam711’s picture

sam711’s picture

Wim Leers’s picture

Nice work, @sam711!

The key thing that's missing here, is test coverage.

  1. +++ b/src/Unstable/Controller/ArgumentResolver/DocumentResolver.php
    @@ -158,4 +185,87 @@ final class DocumentResolver implements ArgumentValueResolverInterface {
    +    if (!empty($data['data']['relationships'])) {
    +      // Turn all single object relationship data fields into an array of
    +      // objects.
    +      $relationships = array_map(function ($relationship) {
    

    This is exactly like \Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer::denormalize()!

    Therefore … this is also related to and would benefit from #3017295: Refactor/clean up JsonApiDocumentTopLevelNormalizer::denormalize() and EntityNormalizer::denormalize().

    Pinging @gabesullice.

  2. +++ b/src/Unstable/Value/NewResourceObject.php
    @@ -37,8 +37,7 @@ final class NewResourceObject extends ResourceObject {
    -      // @todo: handle relationships.
    

    🥳

sam711’s picture

Status: Needs review » Needs work

Thanks for the review @WimLeers.

I'll add tests for POST and PATCH (forgot that PATCH is not supported).

Wim Leers’s picture

Superb, thank you so much! 😃 🙏

sam711’s picture

AddComment Resource Test added. 🤞🏻

sam711’s picture

Status: Needs work » Needs review
sam711’s picture

sam711’s picture

Sorry about that. The patch should now have all the files.

sam711’s picture

sam711’s picture

Assigned: sam711 » Unassigned
mglaman’s picture

Assigned: Unassigned » mglaman

Will review :) The changes in #3104972: Move extracting documents from request objects to a service would cause a lot of disruption on this patch (everything moves to a new file.) So it would be easier to review and get this landed first.

mglaman’s picture

Assigned: mglaman » Unassigned
  1. +++ b/src/Unstable/Controller/ArgumentResolver/DocumentResolver.php
    @@ -115,6 +139,9 @@ final class DocumentResolver implements ArgumentValueResolverInterface {
    +    $primary_data['relationships'] = $this->handleRelationships($decoded);
    
    @@ -158,4 +185,87 @@ final class DocumentResolver implements ArgumentValueResolverInterface {
    +  protected function handleRelationships(array $data): array {
    +    if (!empty($data['data']['relationships'])) {
    ...
    +      }, $data['data']['relationships']);
    

    It looks like we only ever use $data['data']. Perhaps we should only pass the document data? handleRelationships shouldn't need anything else from the top level document.

  2. +++ b/src/Unstable/Controller/ArgumentResolver/DocumentResolver.php
    @@ -158,4 +185,87 @@ final class DocumentResolver implements ArgumentValueResolverInterface {
    +      // Get an array of ids for every relationship.
    +      $relationships = array_map(function ($relationship) {
    

    There's a lot of logic happening in this closure. Should we move to a regular foreach()?

  3. +++ b/src/Unstable/Controller/ArgumentResolver/DocumentResolver.php
    @@ -158,4 +185,87 @@ final class DocumentResolver implements ArgumentValueResolverInterface {
    +        if (empty($relationship['data'][0]['id'])) {
    +          throw new BadRequestHttpException("No ID specified for related resource");
    +        }
    +        $id_list = array_column($relationship['data'], 'id');
    +        if (empty($relationship['data'][0]['type'])) {
    +          throw new BadRequestHttpException("No type specified for related resource");
    +        }
    +        if (!$resource_type = $this->resourceTypeRepository->getByTypeName($relationship['data'][0]['type'])) {
    +          throw new BadRequestHttpException("Invalid type specified for related resource: '" . $relationship['data'][0]['type'] . "'");
    +        }
    

    What if another delta is malformed? Would this catch it?

  4. +++ b/src/Unstable/Controller/ArgumentResolver/DocumentResolver.php
    @@ -158,4 +185,87 @@ final class DocumentResolver implements ArgumentValueResolverInterface {
    +        return array_filter($canonical_ids);
    

    I don't think we'd ever have a nil reference item value to require an array filter

  5. +++ b/src/Unstable/Controller/ArgumentResolver/DocumentResolver.php
    @@ -158,4 +185,87 @@ final class DocumentResolver implements ArgumentValueResolverInterface {
    +    return $relationships ?? [];
    

    Relationships should always be an array if it values is determined from an array map

Here's my initial review. Not enough to warrant "Needs work", but general thoughts.

sam711’s picture

Assigned: Unassigned » sam711

Thanks @mglaman! I'll take a look a bit later today.

sam711’s picture

Interdiff looks long just because I removed the surrounding if (!empty($data['data']['relationships'])) and the indentation changed.

1. Done as you suggest. We could even just pass the relationship...
2. Done.
3. $relationship['data'][0]['type'] is used for all the deltas, so at least the ['id'] of this delta has to exist too. All the delta ids are validated later or ignored if the 'id' doesn't exist.

$id_list = array_column($relationship['data'], 'id');
...
foreach ($id_list as $delta => $uuid) {
        if (!isset($map[$uuid])) {
          // @see \Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer::normalize()
          if ($uuid === 'virtual') {
            continue;
          }
          throw new NotFoundHttpException(sprintf('The resource identified by `%s:%s` (given as a relationship item) could not be found.', $relationship['data'][$delta]['type'], $uuid));
        }

I wonder if different delta types on the same field should be supported.

4.Done
5.That was there because if (!empty($data['data']['relationships'])) could be false. But I've moved this check to the caller so the relationships is not always added to the primary_data.

    if (isset($decoded['data']['relationships'])) {
      $primary_data['relationships'] = $this->handleRelationships($decoded['data']);
    }

If $data['data']['relationships'] is set but empty, the result will be an empty array.

mglaman’s picture

Issue tags: -Needs tests

Thanks @sam711! I'll eyeball it in full later today. But it looks great, we have tests written and I think anything else can be discovered in follow ups.

sam711’s picture

Assigned: sam711 » Unassigned
mglaman’s picture

  1. +++ b/src/Unstable/Controller/ArgumentResolver/DocumentResolver.php
    @@ -158,4 +187,85 @@ final class DocumentResolver implements ArgumentValueResolverInterface {
    +      $id_list = array_column($relationship['data'], 'id');
    

    I totally missed this before, which is how we get all of the IDs for the relationships.

  2. +++ b/src/Unstable/Controller/ArgumentResolver/DocumentResolver.php
    @@ -158,4 +187,85 @@ final class DocumentResolver implements ArgumentValueResolverInterface {
    +        throw new BadRequestHttpException("Invalid type specified for related resource: '" . $relationship['data'][0]['type'] . "'");
    ...
    +        throw new BadRequestHttpException("Invalid type specified for related resource: '" . $relationship['data'][0]['type'] . "'");
    

    nit on commit, probably move to sprintf

  3. +++ b/src/Unstable/Controller/ArgumentResolver/DocumentResolver.php
    @@ -158,4 +187,85 @@ final class DocumentResolver implements ArgumentValueResolverInterface {
    +      $uuid_key = $this->entityTypeManager
    +        ->getDefinition($entity_type_id)->getKey('uuid');
    +      $related_entities = array_values($entity_storage->loadByProperties([$uuid_key => $id_list]));
    

    Hm. Actually, if we don't need the entity manager for anything but this, we could just inject the entity_repository which has loadByUuid.

mglaman’s picture

Hm. Actually, if we don't need the entity manager for anything but this, we could just inject the entity_repository which has loadByUuid.

Oh duh, we need to load multiple.

mglaman’s picture

Status: Needs review » Reviewed & tested by the community
mglaman’s picture

Making sure to credit @Wim Leers

mglaman’s picture

Status: Reviewed & tested by the community » Fixed

Woo! Thank you @sam711!

  • mglaman committed d186f4c on 8.x-1.x authored by sam711
    Issue #3096734 by sam711: Handle relationships when creating resource...
sam711’s picture

Status: Fixed » Needs review

🎉 Thank you both!

sam711’s picture

Status: Needs review » Fixed
Wim Leers’s picture

Don't the changes in #19 mean that #7.1 is no longer true, and hence #3017295: Refactor/clean up JsonApiDocumentTopLevelNormalizer::denormalize() and EntityNormalizer::denormalize() just became more difficult? 🤔

sam711’s picture

Well, I initially thought so but the code is still pretty much the same (although not exactly the same).

I was hoping we can start implementing in JSON:API Resources at least some of the improvements implied in #3017295: Refactor/clean up JsonApiDocumentTopLevelNormalizer::denormalize() and EntityNormalizer::denormalize(). I'm assuming it'll be easier to do it in contrib and may be even convenient.

This code is going to the DocumentExtractor service #3104972: Move extracting documents from request objects to a service that could be considered as one of these improvements, and might need to be refactored there anyway.

Status: Fixed » Closed (fixed)

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