You are here

class EntityResource in Drupal 10

Same name in this branch
  1. 10 core/modules/jsonapi/src/Controller/EntityResource.php \Drupal\jsonapi\Controller\EntityResource
  2. 10 core/modules/rest/src/Plugin/rest/resource/EntityResource.php \Drupal\rest\Plugin\rest\resource\EntityResource
Same name and namespace in other branches
  1. 8 core/modules/jsonapi/src/Controller/EntityResource.php \Drupal\jsonapi\Controller\EntityResource
  2. 9 core/modules/jsonapi/src/Controller/EntityResource.php \Drupal\jsonapi\Controller\EntityResource

Process all entity requests.

@internal JSON:API maintains no PHP API. The API is the HTTP API. This class may change at any time and could break any dependencies on it.

Hierarchy

  • class \Drupal\jsonapi\Controller\EntityResource uses \Drupal\jsonapi\Entity\EntityValidationTrait

Expanded class hierarchy of EntityResource

See also

https://www.drupal.org/project/drupal/issues/3032787

jsonapi.api.php

1 string reference to 'EntityResource'
jsonapi.services.yml in core/modules/jsonapi/jsonapi.services.yml
core/modules/jsonapi/jsonapi.services.yml
1 service uses EntityResource
jsonapi.entity_resource in core/modules/jsonapi/jsonapi.services.yml
Drupal\jsonapi\Controller\EntityResource

File

core/modules/jsonapi/src/Controller/EntityResource.php, line 73

Namespace

Drupal\jsonapi\Controller
View source
class EntityResource {
  use EntityValidationTrait;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The field manager.
   *
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
   */
  protected $fieldManager;

  /**
   * The resource type repository.
   *
   * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface
   */
  protected $resourceTypeRepository;

  /**
   * The renderer.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

  /**
   * The entity repository.
   *
   * @var \Drupal\Core\Entity\EntityRepositoryInterface
   */
  protected $entityRepository;

  /**
   * The include resolver.
   *
   * @var \Drupal\jsonapi\IncludeResolver
   */
  protected $includeResolver;

  /**
   * The JSON:API entity access checker.
   *
   * @var \Drupal\jsonapi\Access\EntityAccessChecker
   */
  protected $entityAccessChecker;

  /**
   * The JSON:API field resolver.
   *
   * @var \Drupal\jsonapi\Context\FieldResolver
   */
  protected $fieldResolver;

  /**
   * The JSON:API serializer.
   *
   * @var \Symfony\Component\Serializer\SerializerInterface|\Symfony\Component\Serializer\Normalizer\DenormalizerInterface
   */
  protected $serializer;

  /**
   * The time service.
   *
   * @var \Drupal\Component\Datetime\TimeInterface
   */
  protected $time;

  /**
   * The current user account.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $user;

  /**
   * Instantiates an EntityResource object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager
   *   The entity type field manager.
   * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
   *   The JSON:API resource type repository.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer.
   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
   *   The entity repository.
   * @param \Drupal\jsonapi\IncludeResolver $include_resolver
   *   The include resolver.
   * @param \Drupal\jsonapi\Access\EntityAccessChecker $entity_access_checker
   *   The JSON:API entity access checker.
   * @param \Drupal\jsonapi\Context\FieldResolver $field_resolver
   *   The JSON:API field resolver.
   * @param \Symfony\Component\Serializer\SerializerInterface|\Symfony\Component\Serializer\Normalizer\DenormalizerInterface $serializer
   *   The JSON:API serializer.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   The time service.
   * @param \Drupal\Core\Session\AccountInterface $user
   *   The current user account.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $field_manager, ResourceTypeRepositoryInterface $resource_type_repository, RendererInterface $renderer, EntityRepositoryInterface $entity_repository, IncludeResolver $include_resolver, EntityAccessChecker $entity_access_checker, FieldResolver $field_resolver, SerializerInterface $serializer, TimeInterface $time, AccountInterface $user) {
    $this->entityTypeManager = $entity_type_manager;
    $this->fieldManager = $field_manager;
    $this->resourceTypeRepository = $resource_type_repository;
    $this->renderer = $renderer;
    $this->entityRepository = $entity_repository;
    $this->includeResolver = $include_resolver;
    $this->entityAccessChecker = $entity_access_checker;
    $this->fieldResolver = $field_resolver;
    $this->serializer = $serializer;
    $this->time = $time;
    $this->user = $user;
  }

  /**
   * Gets the individual entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The loaded entity.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Drupal\jsonapi\ResourceResponse
   *   The response.
   *
   * @throws \Drupal\jsonapi\Exception\EntityAccessDeniedHttpException
   *   Thrown when access to the entity is not allowed.
   */
  public function getIndividual(EntityInterface $entity, Request $request) {
    $resource_object = $this->entityAccessChecker
      ->getAccessCheckedResourceObject($entity);
    if ($resource_object instanceof EntityAccessDeniedHttpException) {
      throw $resource_object;
    }
    $primary_data = new ResourceObjectData([
      $resource_object,
    ], 1);
    $response = $this
      ->buildWrappedResponse($primary_data, $request, $this
      ->getIncludes($request, $primary_data));
    return $response;
  }

  /**
   * Creates an individual entity.
   *
   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
   *   The JSON:API resource type for the request to be served.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Drupal\jsonapi\ResourceResponse
   *   The response.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\ConflictHttpException
   *   Thrown when the entity already exists.
   * @throws \Drupal\jsonapi\Exception\UnprocessableHttpEntityException
   *   Thrown when the entity does not pass validation.
   */
  public function createIndividual(ResourceType $resource_type, Request $request) {
    $parsed_entity = $this
      ->deserialize($resource_type, $request, JsonApiDocumentTopLevel::class);
    if ($parsed_entity instanceof FieldableEntityInterface) {

      // Only check 'edit' permissions for fields that were actually submitted
      // by the user. Field access makes no distinction between 'create' and
      // 'update', so the 'edit' operation is used here.
      $document = Json::decode($request
        ->getContent());
      $field_mapping = array_map(function (ResourceTypeField $field) {
        return $field
          ->getPublicName();
      }, $resource_type
        ->getFields());

      // User resource objects contain a read-only attribute that is not a
      // real field on the user entity type.
      // @see \Drupal\jsonapi\JsonApiResource\ResourceObject::extractContentEntityFields()
      // @todo: eliminate this special casing in https://www.drupal.org/project/drupal/issues/3079254.
      if ($resource_type
        ->getEntityTypeId() === 'user') {
        $field_mapping = array_diff($field_mapping, [
          $resource_type
            ->getPublicName('display_name'),
        ]);
      }
      foreach ([
        'attributes',
        'relationships',
      ] as $data_member_name) {
        if (isset($document['data'][$data_member_name])) {
          foreach (array_intersect_key(array_flip($field_mapping), $document['data'][$data_member_name]) as $internal_field_name) {
            $field_access = $parsed_entity
              ->get($internal_field_name)
              ->access('edit', NULL, TRUE);
            if (!$field_access
              ->isAllowed()) {
              $public_field_name = $field_mapping[$internal_field_name];
              throw new EntityAccessDeniedHttpException(NULL, $field_access, "/data/{$data_member_name}/{$public_field_name}", sprintf('The current user is not allowed to POST the selected field (%s).', $public_field_name));
            }
          }
        }
      }
    }
    static::validate($parsed_entity);

    // Return a 409 Conflict response in accordance with the JSON:API spec. See
    // http://jsonapi.org/format/#crud-creating-responses-409.
    if ($this
      ->entityExists($parsed_entity)) {
      throw new ConflictHttpException('Conflict: Entity already exists.');
    }
    $parsed_entity
      ->save();

    // Build response object.
    $resource_object = ResourceObject::createFromEntity($resource_type, $parsed_entity);
    $primary_data = new ResourceObjectData([
      $resource_object,
    ], 1);
    $response = $this
      ->buildWrappedResponse($primary_data, $request, $this
      ->getIncludes($request, $primary_data), 201);

    // According to JSON:API specification, when a new entity was created
    // we should send "Location" header to the frontend.
    if ($resource_type
      ->isLocatable()) {
      $url = $resource_object
        ->toUrl()
        ->setAbsolute()
        ->toString(TRUE);
      $response->headers
        ->set('Location', $url
        ->getGeneratedUrl());
    }

    // Return response object with updated headers info.
    return $response;
  }

  /**
   * Patches an individual entity.
   *
   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
   *   The JSON:API resource type for the request to be served.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The loaded entity.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Drupal\jsonapi\ResourceResponse
   *   The response.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
   *   Thrown when the selected entity does not match the id in th payload.
   * @throws \Drupal\jsonapi\Exception\UnprocessableHttpEntityException
   *   Thrown when the patched entity does not pass validation.
   */
  public function patchIndividual(ResourceType $resource_type, EntityInterface $entity, Request $request) {
    if ($entity instanceof RevisionableInterface && !($entity
      ->isLatestRevision() && $entity
      ->isDefaultRevision())) {
      throw new BadRequestHttpException('Updating a resource object that has a working copy is not yet supported. See https://www.drupal.org/project/drupal/issues/2795279.');
    }
    $parsed_entity = $this
      ->deserialize($resource_type, $request, JsonApiDocumentTopLevel::class);
    $body = Json::decode($request
      ->getContent());
    $data = $body['data'];
    if ($data['id'] != $entity
      ->uuid()) {
      throw new BadRequestHttpException(sprintf('The selected entity (%s) does not match the ID in the payload (%s).', $entity
        ->uuid(), $data['id']));
    }
    $data += [
      'attributes' => [],
      'relationships' => [],
    ];
    $field_names = array_map([
      $resource_type,
      'getInternalName',
    ], array_merge(array_keys($data['attributes']), array_keys($data['relationships'])));

    // User resource objects contain a read-only attribute that is not a real
    // field on the user entity type.
    // @see \Drupal\jsonapi\JsonApiResource\ResourceObject::extractContentEntityFields()
    // @todo: eliminate this special casing in https://www.drupal.org/project/drupal/issues/3079254.
    if ($entity
      ->getEntityTypeId() === 'user') {
      $field_names = array_diff($field_names, [
        $resource_type
          ->getPublicName('display_name'),
      ]);
    }
    array_reduce($field_names, function (EntityInterface $destination, $field_name) use ($resource_type, $parsed_entity) {
      $this
        ->updateEntityField($resource_type, $parsed_entity, $destination, $field_name);
      return $destination;
    }, $entity);
    static::validate($entity, $field_names);

    // Set revision data details for revisionable entities.
    if ($entity
      ->getEntityType()
      ->isRevisionable()) {
      if ($bundle_entity_type = $entity
        ->getEntityType()
        ->getBundleEntityType()) {
        $bundle_entity = $this->entityTypeManager
          ->getStorage($bundle_entity_type)
          ->load($entity
          ->bundle());
        if ($bundle_entity instanceof RevisionableEntityBundleInterface) {
          $entity
            ->setNewRevision($bundle_entity
            ->shouldCreateNewRevision());
        }
      }
      if ($entity instanceof RevisionLogInterface && $entity
        ->isNewRevision()) {
        $entity
          ->setRevisionUserId($this->user
          ->id());
        $entity
          ->setRevisionCreationTime($this->time
          ->getRequestTime());
      }
    }
    $entity
      ->save();
    $primary_data = new ResourceObjectData([
      ResourceObject::createFromEntity($resource_type, $entity),
    ], 1);
    return $this
      ->buildWrappedResponse($primary_data, $request, $this
      ->getIncludes($request, $primary_data));
  }

  /**
   * Deletes an individual entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The loaded entity.
   *
   * @return \Drupal\jsonapi\ResourceResponse
   *   The response.
   */
  public function deleteIndividual(EntityInterface $entity) {

    // @todo Replace with entity handlers in: https://www.drupal.org/project/drupal/issues/3230434
    if ($entity
      ->getEntityTypeId() === 'user') {
      $cancel_method = \Drupal::service('config.factory')
        ->get('user.settings')
        ->get('cancel_method');

      // Allow other modules to act.
      user_cancel([], $entity
        ->id(), $cancel_method);

      // Since user_cancel() is not invoked via Form API, batch processing
      // needs to be invoked manually.
      $batch =& batch_get();

      // Mark this batch as non-progressive to bypass the progress bar and
      // redirect.
      $batch['progressive'] = FALSE;
      batch_process();
    }
    else {
      $entity
        ->delete();
    }
    return new ResourceResponse(NULL, 204);
  }

  /**
   * Gets the collection of entities.
   *
   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
   *   The JSON:API resource type for the request to be served.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Drupal\jsonapi\ResourceResponse
   *   The response.
   *
   * @throws \Drupal\Core\Http\Exception\CacheableBadRequestHttpException
   *   Thrown when filtering on a config entity which does not support it.
   */
  public function getCollection(ResourceType $resource_type, Request $request) {

    // Instantiate the query for the filtering.
    $entity_type_id = $resource_type
      ->getEntityTypeId();
    $params = $this
      ->getJsonApiParams($request, $resource_type);
    $query_cacheability = new CacheableMetadata();
    $query = $this
      ->getCollectionQuery($resource_type, $params, $query_cacheability);

    // If the request is for the latest revision, toggle it on entity query.
    if ($request
      ->get(ResourceVersionRouteEnhancer::WORKING_COPIES_REQUESTED, FALSE)) {
      $query
        ->latestRevision();
    }
    try {
      $results = $this
        ->executeQueryInRenderContext($query, $query_cacheability);
    } catch (\LogicException $e) {

      // Ensure good DX when an entity query involves a config entity type.
      // For example: getting users with a particular role, which is a config
      // entity type: https://www.drupal.org/project/drupal/issues/2959445.
      // @todo Remove the message parsing in https://www.drupal.org/project/drupal/issues/3028967.
      if (strpos($e
        ->getMessage(), 'Getting the base fields is not supported for entity type') === 0) {
        preg_match('/entity type (.*)\\./', $e
          ->getMessage(), $matches);
        $config_entity_type_id = $matches[1];
        $cacheability = (new CacheableMetadata())
          ->addCacheContexts([
          'url.path',
          'url.query_args:filter',
        ]);
        throw new CacheableBadRequestHttpException($cacheability, sprintf("Filtering on config entities is not supported by Drupal's entity API. You tried to filter on a %s config entity.", $config_entity_type_id));
      }
      else {
        throw $e;
      }
    }
    $storage = $this->entityTypeManager
      ->getStorage($entity_type_id);

    // We request N+1 items to find out if there is a next page for the pager.
    // We may need to remove that extra item before loading the entities.
    $pager_size = $query
      ->getMetaData('pager_size');
    if ($has_next_page = $pager_size < count($results)) {

      // Drop the last result.
      array_pop($results);
    }

    // Each item of the collection data contains an array with 'entity' and
    // 'access' elements.
    $collection_data = $this
      ->loadEntitiesWithAccess($storage, $results, $request
      ->get(ResourceVersionRouteEnhancer::WORKING_COPIES_REQUESTED, FALSE));
    $primary_data = new ResourceObjectData($collection_data);
    $primary_data
      ->setHasNextPage($has_next_page);

    // Calculate all the results and pass into a JSON:API Data object.
    $count_query_cacheability = new CacheableMetadata();
    if ($resource_type
      ->includeCount()) {
      $count_query = $this
        ->getCollectionCountQuery($resource_type, $params, $count_query_cacheability);
      $total_results = $this
        ->executeQueryInRenderContext($count_query, $count_query_cacheability);
      $primary_data
        ->setTotalCount($total_results);
    }
    $response = $this
      ->respondWithCollection($primary_data, $this
      ->getIncludes($request, $primary_data), $request, $resource_type, $params[OffsetPage::KEY_NAME]);
    $response
      ->addCacheableDependency($query_cacheability);
    $response
      ->addCacheableDependency($count_query_cacheability);
    $response
      ->addCacheableDependency((new CacheableMetadata())
      ->addCacheContexts([
      'url.query_args:filter',
      'url.query_args:sort',
      'url.query_args:page',
    ]));
    if ($resource_type
      ->isVersionable()) {
      $response
        ->addCacheableDependency((new CacheableMetadata())
        ->addCacheContexts([
        ResourceVersionRouteEnhancer::CACHE_CONTEXT,
      ]));
    }
    return $response;
  }

  /**
   * Executes the query in a render context, to catch bubbled cacheability.
   *
   * @param \Drupal\Core\Entity\Query\QueryInterface $query
   *   The query to execute to get the return results.
   * @param \Drupal\Core\Cache\CacheableMetadata $query_cacheability
   *   The value object to carry the query cacheability.
   *
   * @return int|array
   *   Returns an integer for count queries or an array of IDs. The values of
   *   the array are always entity IDs. The keys will be revision IDs if the
   *   entity supports revision and entity IDs if not.
   *
   * @see node_query_node_access_alter()
   * @see https://www.drupal.org/project/drupal/issues/2557815
   * @see https://www.drupal.org/project/drupal/issues/2794385
   * @todo Remove this after https://www.drupal.org/project/drupal/issues/3028976 is fixed.
   */
  protected function executeQueryInRenderContext(QueryInterface $query, CacheableMetadata $query_cacheability) {
    $context = new RenderContext();
    $results = $this->renderer
      ->executeInRenderContext($context, function () use ($query) {
      return $query
        ->execute();
    });
    if (!$context
      ->isEmpty()) {
      $query_cacheability
        ->addCacheableDependency($context
        ->pop());
    }
    return $results;
  }

  /**
   * Gets the related resource.
   *
   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
   *   The JSON:API resource type for the request to be served.
   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
   *   The requested entity.
   * @param string $related
   *   The related field name.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Drupal\jsonapi\ResourceResponse
   *   The response.
   */
  public function getRelated(ResourceType $resource_type, FieldableEntityInterface $entity, $related, Request $request) {

    /** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */
    $resource_relationship = $resource_type
      ->getFieldByPublicName($related);
    $field_list = $entity
      ->get($resource_relationship
      ->getInternalName());

    // Remove the entities pointing to a resource that may be disabled. Even
    // though the normalizer skips disabled references, we can avoid unnecessary
    // work by checking here too.

    /** @var \Drupal\Core\Entity\EntityInterface[] $referenced_entities */
    $referenced_entities = array_filter($field_list
      ->referencedEntities(), function (EntityInterface $entity) {
      return (bool) $this->resourceTypeRepository
        ->get($entity
        ->getEntityTypeId(), $entity
        ->bundle());
    });
    $collection_data = [];
    foreach ($referenced_entities as $referenced_entity) {
      $collection_data[] = $this->entityAccessChecker
        ->getAccessCheckedResourceObject($referenced_entity);
    }
    $primary_data = new ResourceObjectData($collection_data, $resource_relationship
      ->hasOne() ? 1 : -1);
    $response = $this
      ->buildWrappedResponse($primary_data, $request, $this
      ->getIncludes($request, $primary_data));

    // $response does not contain the entity list cache tag. We add the
    // cacheable metadata for the finite list of entities in the relationship.
    if ($response instanceof CacheableResponseInterface) {
      $response
        ->addCacheableDependency($entity);
    }
    return $response;
  }

  /**
   * Gets the relationship of an entity.
   *
   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
   *   The base JSON:API resource type for the request to be served.
   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
   *   The requested entity.
   * @param string $related
   *   The related field name.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   * @param int $response_code
   *   The response code. Defaults to 200.
   *
   * @return \Drupal\jsonapi\ResourceResponse
   *   The response.
   */
  public function getRelationship(ResourceType $resource_type, FieldableEntityInterface $entity, $related, Request $request, $response_code = 200) {

    /** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */
    $field_list = $entity
      ->get($resource_type
      ->getInternalName($related));

    // Access will have already been checked by the RelationshipRouteAccessCheck
    // service, so we don't need to call ::getAccessCheckedResourceObject().
    $resource_object = ResourceObject::createFromEntity($resource_type, $entity);
    $relationship = Relationship::createFromEntityReferenceField($resource_object, $field_list);
    $response = $this
      ->buildWrappedResponse($relationship, $request, $this
      ->getIncludes($request, $resource_object), $response_code);

    // Add the host entity as a cacheable dependency.
    if ($response instanceof CacheableResponseInterface) {
      $response
        ->addCacheableDependency($entity);
    }
    return $response;
  }

  /**
   * Adds a relationship to a to-many relationship.
   *
   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
   *   The base JSON:API resource type for the request to be served.
   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
   *   The requested entity.
   * @param string $related
   *   The related field name.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Drupal\jsonapi\ResourceResponse
   *   The response.
   *
   * @throws \Drupal\jsonapi\Exception\EntityAccessDeniedHttpException
   *   Thrown when the current user is not allowed to PATCH the selected
   *   field(s).
   * @throws \Symfony\Component\HttpKernel\Exception\ConflictHttpException
   *   Thrown when POSTing to a "to-one" relationship.
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   Thrown when the underlying entity cannot be saved.
   * @throws \Drupal\jsonapi\Exception\UnprocessableHttpEntityException
   *   Thrown when the updated entity does not pass validation.
   */
  public function addToRelationshipData(ResourceType $resource_type, FieldableEntityInterface $entity, $related, Request $request) {
    $resource_identifiers = $this
      ->deserialize($resource_type, $request, ResourceIdentifier::class, $related);
    $internal_relationship_field_name = $resource_type
      ->getInternalName($related);

    // According to the specification, you are only allowed to POST to a
    // relationship if it is a to-many relationship.

    /** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */
    $field_list = $entity->{$internal_relationship_field_name};

    /** @var \Drupal\field\Entity\FieldConfig $field_definition */
    $field_definition = $field_list
      ->getFieldDefinition();
    $is_multiple = $field_definition
      ->getFieldStorageDefinition()
      ->isMultiple();
    if (!$is_multiple) {
      throw new ConflictHttpException(sprintf('You can only POST to to-many relationships. %s is a to-one relationship.', $related));
    }
    $original_resource_identifiers = ResourceIdentifier::toResourceIdentifiersWithArityRequired($field_list);
    $new_resource_identifiers = array_udiff(ResourceIdentifier::deduplicate(array_merge($original_resource_identifiers, $resource_identifiers)), $original_resource_identifiers, [
      ResourceIdentifier::class,
      'compare',
    ]);

    // There are no relationships that need to be added so we can exit early.
    if (empty($new_resource_identifiers)) {
      $status = static::relationshipResponseRequiresBody($resource_identifiers, $original_resource_identifiers) ? 200 : 204;
      return $this
        ->getRelationship($resource_type, $entity, $related, $request, $status);
    }
    $main_property_name = $field_definition
      ->getItemDefinition()
      ->getMainPropertyName();
    foreach ($new_resource_identifiers as $new_resource_identifier) {
      $new_field_value = [
        $main_property_name => $this
          ->getEntityFromResourceIdentifier($new_resource_identifier)
          ->id(),
      ];

      // Remove `arity` from the received extra properties, otherwise this
      // will fail field validation.
      $new_field_value += array_diff_key($new_resource_identifier
        ->getMeta(), array_flip([
        ResourceIdentifier::ARITY_KEY,
      ]));
      $field_list
        ->appendItem($new_field_value);
    }
    $this
      ->validate($entity);
    $entity
      ->save();
    $final_resource_identifiers = ResourceIdentifier::toResourceIdentifiersWithArityRequired($field_list);
    $status = static::relationshipResponseRequiresBody($resource_identifiers, $final_resource_identifiers) ? 200 : 204;
    return $this
      ->getRelationship($resource_type, $entity, $related, $request, $status);
  }

  /**
   * Updates the relationship of an entity.
   *
   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
   *   The base JSON:API resource type for the request to be served.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The requested entity.
   * @param string $related
   *   The related field name.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Drupal\jsonapi\ResourceResponse
   *   The response.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   Thrown when the underlying entity cannot be saved.
   * @throws \Drupal\jsonapi\Exception\UnprocessableHttpEntityException
   *   Thrown when the updated entity does not pass validation.
   */
  public function replaceRelationshipData(ResourceType $resource_type, EntityInterface $entity, $related, Request $request) {

    /** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $resource_identifiers */
    $resource_identifiers = $this
      ->deserialize($resource_type, $request, ResourceIdentifier::class, $related);
    $internal_relationship_field_name = $resource_type
      ->getInternalName($related);

    // According to the specification, PATCH works a little bit different if the
    // relationship is to-one or to-many.

    /** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */
    $field_list = $entity->{$internal_relationship_field_name};
    $field_definition = $field_list
      ->getFieldDefinition();
    $is_multiple = $field_definition
      ->getFieldStorageDefinition()
      ->isMultiple();
    $method = $is_multiple ? 'doPatchMultipleRelationship' : 'doPatchIndividualRelationship';
    $this
      ->{$method}($entity, $resource_identifiers, $field_definition);
    $this
      ->validate($entity);
    $entity
      ->save();
    $requires_response = static::relationshipResponseRequiresBody($resource_identifiers, ResourceIdentifier::toResourceIdentifiersWithArityRequired($field_list));
    return $this
      ->getRelationship($resource_type, $entity, $related, $request, $requires_response ? 200 : 204);
  }

  /**
   * Update a to-one relationship.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The requested entity.
   * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier[] $resource_identifiers
   *   The client-sent resource identifiers which should be set on the given
   *   entity. Should be an empty array or an array with a single value.
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
   *   The field definition of the entity field to be updated.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
   *   Thrown when a "to-one" relationship is not provided.
   */
  protected function doPatchIndividualRelationship(EntityInterface $entity, array $resource_identifiers, FieldDefinitionInterface $field_definition) {
    if (count($resource_identifiers) > 1) {
      throw new BadRequestHttpException(sprintf('Provide a single relationship so to-one relationship fields (%s).', $field_definition
        ->getName()));
    }
    $this
      ->doPatchMultipleRelationship($entity, $resource_identifiers, $field_definition);
  }

  /**
   * Update a to-many relationship.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The requested entity.
   * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier[] $resource_identifiers
   *   The client-sent resource identifiers which should be set on the given
   *   entity.
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
   *   The field definition of the entity field to be updated.
   */
  protected function doPatchMultipleRelationship(EntityInterface $entity, array $resource_identifiers, FieldDefinitionInterface $field_definition) {
    $main_property_name = $field_definition
      ->getItemDefinition()
      ->getMainPropertyName();
    $entity->{$field_definition
      ->getName()} = array_map(function (ResourceIdentifier $resource_identifier) use ($main_property_name) {
      $field_properties = [
        $main_property_name => $this
          ->getEntityFromResourceIdentifier($resource_identifier)
          ->id(),
      ];

      // Remove `arity` from the received extra properties, otherwise this
      // will fail field validation.
      $field_properties += array_diff_key($resource_identifier
        ->getMeta(), array_flip([
        ResourceIdentifier::ARITY_KEY,
      ]));
      return $field_properties;
    }, $resource_identifiers);
  }

  /**
   * Deletes the relationship of an entity.
   *
   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
   *   The base JSON:API resource type for the request to be served.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The requested entity.
   * @param string $related
   *   The related field name.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Drupal\jsonapi\ResourceResponse
   *   The response.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
   *   Thrown when not body was provided for the DELETE operation.
   * @throws \Symfony\Component\HttpKernel\Exception\ConflictHttpException
   *   Thrown when deleting a "to-one" relationship.
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   Thrown when the underlying entity cannot be saved.
   */
  public function removeFromRelationshipData(ResourceType $resource_type, EntityInterface $entity, $related, Request $request) {
    $resource_identifiers = $this
      ->deserialize($resource_type, $request, ResourceIdentifier::class, $related);
    $internal_relationship_field_name = $resource_type
      ->getInternalName($related);

    /** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */
    $field_list = $entity->{$internal_relationship_field_name};
    $is_multiple = $field_list
      ->getFieldDefinition()
      ->getFieldStorageDefinition()
      ->isMultiple();
    if (!$is_multiple) {
      throw new ConflictHttpException(sprintf('You can only DELETE from to-many relationships. %s is a to-one relationship.', $related));
    }

    // Compute the list of current values and remove the ones in the payload.
    $original_resource_identifiers = ResourceIdentifier::toResourceIdentifiersWithArityRequired($field_list);
    $removed_resource_identifiers = array_uintersect($resource_identifiers, $original_resource_identifiers, [
      ResourceIdentifier::class,
      'compare',
    ]);
    $deltas_to_be_removed = [];
    foreach ($removed_resource_identifiers as $removed_resource_identifier) {
      foreach ($original_resource_identifiers as $delta => $existing_resource_identifier) {

        // Identify the field item deltas which should be removed.
        if (ResourceIdentifier::isDuplicate($removed_resource_identifier, $existing_resource_identifier)) {
          $deltas_to_be_removed[] = $delta;
        }
      }
    }

    // Field item deltas are reset when an item is removed. This removes
    // items in descending order so that the deltas yet to be removed will
    // continue to exist.
    rsort($deltas_to_be_removed);
    foreach ($deltas_to_be_removed as $delta) {
      $field_list
        ->removeItem($delta);
    }

    // Save the entity and return the response object.
    static::validate($entity);
    $entity
      ->save();
    return $this
      ->getRelationship($resource_type, $entity, $related, $request, 204);
  }

  /**
   * Deserializes a request body, if any.
   *
   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
   *   The JSON:API resource type for the current request.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   * @param string $class
   *   The class into which the request data needs to be deserialized.
   * @param string $relationship_field_name
   *   The public relationship field name of the data to be deserialized if the
   *   incoming request is for a relationship update. Not required for non-
   *   relationship requests.
   *
   * @return array
   *   An object normalization.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
   *   Thrown if the request body cannot be decoded, or when no request body was
   *   provided with a POST or PATCH request.
   * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
   *   Thrown if the request body cannot be denormalized.
   */
  protected function deserialize(ResourceType $resource_type, Request $request, $class, $relationship_field_name = NULL) {
    assert($class === JsonApiDocumentTopLevel::class || $class === ResourceIdentifier::class && !empty($relationship_field_name) && is_string($relationship_field_name));
    $received = (string) $request
      ->getContent();
    if (!$received) {
      assert($request
        ->isMethod('POST') || $request
        ->isMethod('PATCH') || $request
        ->isMethod('DELETE'));
      if ($request
        ->isMethod('DELETE') && $relationship_field_name) {
        throw new BadRequestHttpException(sprintf('You need to provide a body for DELETE operations on a relationship (%s).', $relationship_field_name));
      }
      else {
        throw new BadRequestHttpException('Empty request body.');
      }
    }

    // First decode the request data. We can then determine if the serialized
    // data was malformed.
    try {
      $decoded = $this->serializer
        ->decode($received, 'api_json');
    } catch (UnexpectedValueException $e) {

      // If an exception was thrown at this stage, there was a problem decoding
      // the data. Throw a 400 HTTP exception.
      throw new BadRequestHttpException($e
        ->getMessage());
    }
    try {
      $context = [
        'resource_type' => $resource_type,
      ];
      if ($relationship_field_name) {
        $context['related'] = $resource_type
          ->getInternalName($relationship_field_name);
      }
      return $this->serializer
        ->denormalize($decoded, $class, 'api_json', $context);
    } catch (UnexpectedValueException $e) {
      throw new UnprocessableEntityHttpException($e
        ->getMessage());
    } catch (InvalidArgumentException $e) {
      throw new UnprocessableEntityHttpException($e
        ->getMessage());
    }
  }

  /**
   * Gets a basic query for a collection.
   *
   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
   *   The base JSON:API resource type for the query.
   * @param array $params
   *   The parameters for the query.
   * @param \Drupal\Core\Cache\CacheableMetadata $query_cacheability
   *   Collects cacheability for the query.
   *
   * @return \Drupal\Core\Entity\Query\QueryInterface
   *   A new query.
   */
  protected function getCollectionQuery(ResourceType $resource_type, array $params, CacheableMetadata $query_cacheability) {
    $entity_type = $this->entityTypeManager
      ->getDefinition($resource_type
      ->getEntityTypeId());
    $entity_storage = $this->entityTypeManager
      ->getStorage($resource_type
      ->getEntityTypeId());
    $query = $entity_storage
      ->getQuery();

    // Ensure that access checking is performed on the query.
    $query
      ->accessCheck(TRUE);

    // Compute and apply an entity query condition from the filter parameter.
    if (isset($params[Filter::KEY_NAME]) && ($filter = $params[Filter::KEY_NAME])) {
      $query
        ->condition($filter
        ->queryCondition($query));
      TemporaryQueryGuard::setFieldManager($this->fieldManager);
      TemporaryQueryGuard::setModuleHandler(\Drupal::moduleHandler());
      TemporaryQueryGuard::applyAccessControls($filter, $query, $query_cacheability);
    }

    // Apply any sorts to the entity query.
    if (isset($params[Sort::KEY_NAME]) && ($sort = $params[Sort::KEY_NAME])) {
      foreach ($sort
        ->fields() as $field) {
        $path = $this->fieldResolver
          ->resolveInternalEntityQueryPath($resource_type, $field[Sort::PATH_KEY]);
        $direction = $field[Sort::DIRECTION_KEY] ?? 'ASC';
        $langcode = $field[Sort::LANGUAGE_KEY] ?? NULL;
        $query
          ->sort($path, $direction, $langcode);
      }
    }

    // Apply any pagination options to the query.
    if (isset($params[OffsetPage::KEY_NAME])) {
      $pagination = $params[OffsetPage::KEY_NAME];
    }
    else {
      $pagination = new OffsetPage(OffsetPage::DEFAULT_OFFSET, OffsetPage::SIZE_MAX);
    }

    // Add one extra element to the page to see if there are more pages needed.
    $query
      ->range($pagination
      ->getOffset(), $pagination
      ->getSize() + 1);
    $query
      ->addMetaData('pager_size', (int) $pagination
      ->getSize());

    // Limit this query to the bundle type for this resource.
    $bundle = $resource_type
      ->getBundle();
    if ($bundle && ($bundle_key = $entity_type
      ->getKey('bundle'))) {
      $query
        ->condition($bundle_key, $bundle);
    }
    return $query;
  }

  /**
   * Gets a basic query for a collection count.
   *
   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
   *   The base JSON:API resource type for the query.
   * @param array $params
   *   The parameters for the query.
   * @param \Drupal\Core\Cache\CacheableMetadata $query_cacheability
   *   Collects cacheability for the query.
   *
   * @return \Drupal\Core\Entity\Query\QueryInterface
   *   A new query.
   */
  protected function getCollectionCountQuery(ResourceType $resource_type, array $params, CacheableMetadata $query_cacheability) {

    // Reset the range to get all the available results.
    return $this
      ->getCollectionQuery($resource_type, $params, $query_cacheability)
      ->range()
      ->count();
  }

  /**
   * Loads the entity targeted by a resource identifier.
   *
   * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $resource_identifier
   *   A resource identifier.
   *
   * @return \Drupal\Core\Entity\EntityInterface
   *   The entity targeted by a resource identifier.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
   *   Thrown if the given resource identifier targets a resource type or
   *   resource which does not exist.
   */
  protected function getEntityFromResourceIdentifier(ResourceIdentifier $resource_identifier) {
    $resource_type_name = $resource_identifier
      ->getTypeName();
    if (!($target_resource_type = $this->resourceTypeRepository
      ->getByTypeName($resource_type_name))) {
      throw new BadRequestHttpException("The resource type `{$resource_type_name}` does not exist.");
    }
    $id = $resource_identifier
      ->getId();
    if (!($targeted_resource = $this->entityRepository
      ->loadEntityByUuid($target_resource_type
      ->getEntityTypeId(), $id))) {
      throw new BadRequestHttpException("The targeted `{$resource_type_name}` resource with ID `{$id}` does not exist.");
    }
    return $targeted_resource;
  }

  /**
   * Determines if the client needs to be updated with new relationship data.
   *
   * @param array $received_resource_identifiers
   *   The array of resource identifiers given by the client.
   * @param array $final_resource_identifiers
   *   The final array of resource identifiers after applying the requested
   *   changes.
   *
   * @return bool
   *   Whether the final array of resource identifiers is different than the
   *   client-sent data.
   */
  protected static function relationshipResponseRequiresBody(array $received_resource_identifiers, array $final_resource_identifiers) {
    return !empty(array_udiff($final_resource_identifiers, $received_resource_identifiers, [
      ResourceIdentifier::class,
      'compare',
    ]));
  }

  /**
   * Builds a response with the appropriate wrapped document.
   *
   * @param \Drupal\jsonapi\JsonApiResource\TopLevelDataInterface $data
   *   The data to wrap.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   * @param \Drupal\jsonapi\JsonApiResource\IncludedData $includes
   *   The resources to be included in the document. Use NullData if
   *   there should be no included resources in the document.
   * @param int $response_code
   *   The response code.
   * @param array $headers
   *   An array of response headers.
   * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $links
   *   The URLs to which to link. A 'self' link is added automatically.
   * @param array $meta
   *   (optional) The top-level metadata.
   *
   * @return \Drupal\jsonapi\ResourceResponse
   *   The response.
   */
  protected function buildWrappedResponse(TopLevelDataInterface $data, Request $request, IncludedData $includes, $response_code = 200, array $headers = [], LinkCollection $links = NULL, array $meta = []) {
    $links = $links ?: new LinkCollection([]);
    if (!$links
      ->hasLinkWithKey('self')) {
      $self_link = new Link(new CacheableMetadata(), self::getRequestLink($request), 'self');
      $links = $links
        ->withLink('self', $self_link);
    }
    $document = new JsonApiDocumentTopLevel($data, $includes, $links, $meta);
    if (!$request
      ->isMethodCacheable()) {
      return new ResourceResponse($document, $response_code, $headers);
    }
    $response = new CacheableResourceResponse($document, $response_code, $headers);
    $cacheability = (new CacheableMetadata())
      ->addCacheContexts([
      // Make sure that different sparse fieldsets are cached differently.
      'url.query_args:fields',
      // Make sure that different sets of includes are cached differently.
      'url.query_args:include',
    ]);
    $response
      ->addCacheableDependency($cacheability);
    return $response;
  }

  /**
   * Respond with an entity collection.
   *
   * @param \Drupal\jsonapi\JsonApiResource\ResourceObjectData $primary_data
   *   The collection of entities.
   * @param \Drupal\jsonapi\JsonApiResource\IncludedData|\Drupal\jsonapi\JsonApiResource\NullIncludedData $includes
   *   The resources to be included in the document.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
   *   The base JSON:API resource type for the request to be served.
   * @param \Drupal\jsonapi\Query\OffsetPage $page_param
   *   The pagination parameter for the requested collection.
   *
   * @return \Drupal\jsonapi\ResourceResponse
   *   The response.
   */
  protected function respondWithCollection(ResourceObjectData $primary_data, Data $includes, Request $request, ResourceType $resource_type, OffsetPage $page_param) {
    assert(Inspector::assertAllObjects([
      $includes,
    ], IncludedData::class, NullIncludedData::class));
    $link_context = [
      'has_next_page' => $primary_data
        ->hasNextPage(),
    ];
    $meta = [];
    if ($resource_type
      ->includeCount()) {
      $link_context['total_count'] = $meta['count'] = $primary_data
        ->getTotalCount();
    }
    $collection_links = self::getPagerLinks($request, $page_param, $link_context);
    $response = $this
      ->buildWrappedResponse($primary_data, $request, $includes, 200, [], $collection_links, $meta);

    // When a new change to any entity in the resource happens, we cannot ensure
    // the validity of this cached list. Add the list tag to deal with that.
    $list_tag = $this->entityTypeManager
      ->getDefinition($resource_type
      ->getEntityTypeId())
      ->getListCacheTags();
    $response
      ->getCacheableMetadata()
      ->addCacheTags($list_tag);
    foreach ($primary_data as $entity) {
      $response
        ->addCacheableDependency($entity);
    }
    return $response;
  }

  /**
   * Takes a field from the origin entity and puts it to the destination entity.
   *
   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
   *   The JSON:API resource type of the entity to be updated.
   * @param \Drupal\Core\Entity\EntityInterface $origin
   *   The entity that contains the field values.
   * @param \Drupal\Core\Entity\EntityInterface $destination
   *   The entity that needs to be updated.
   * @param string $field_name
   *   The name of the field to extract and update.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
   *   Thrown when the serialized and destination entities are of different
   *   types.
   */
  protected function updateEntityField(ResourceType $resource_type, EntityInterface $origin, EntityInterface $destination, $field_name) {

    // The update is different for configuration entities and content entities.
    if ($origin instanceof ContentEntityInterface && $destination instanceof ContentEntityInterface) {

      // First scenario: both are content entities.
      $field_name = $resource_type
        ->getInternalName($field_name);
      $destination_field_list = $destination
        ->get($field_name);
      $origin_field_list = $origin
        ->get($field_name);
      if ($this
        ->checkPatchFieldAccess($destination_field_list, $origin_field_list)) {
        $destination
          ->set($field_name, $origin_field_list
          ->getValue());
      }
    }
    elseif ($origin instanceof ConfigEntityInterface && $destination instanceof ConfigEntityInterface) {

      // Second scenario: both are config entities.
      $destination
        ->set($field_name, $origin
        ->get($field_name));
    }
    else {
      throw new BadRequestHttpException('The serialized entity and the destination entity are of different types.');
    }
  }

  /**
   * Gets includes for the given response data.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   * @param \Drupal\jsonapi\JsonApiResource\ResourceObject|\Drupal\jsonapi\JsonApiResource\ResourceObjectData $data
   *   The response data from which to resolve includes.
   *
   * @return \Drupal\jsonapi\JsonApiResource\Data
   *   A Data object to be included or a NullData object if the request does not
   *   specify any include paths.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function getIncludes(Request $request, $data) {
    assert($data instanceof ResourceObject || $data instanceof ResourceObjectData);
    return $request->query
      ->has('include') && ($include_parameter = $request->query
      ->get('include')) && !empty($include_parameter) ? $this->includeResolver
      ->resolve($data, $include_parameter) : new NullIncludedData();
  }

  /**
   * Checks whether the given field should be PATCHed.
   *
   * @param \Drupal\Core\Field\FieldItemListInterface $original_field
   *   The original (stored) value for the field.
   * @param \Drupal\Core\Field\FieldItemListInterface $received_field
   *   The received value for the field.
   *
   * @return bool
   *   Whether the field should be PATCHed or not.
   *
   * @throws \Drupal\jsonapi\Exception\EntityAccessDeniedHttpException
   *   Thrown when the user sending the request is not allowed to update the
   *   field. Only thrown when the user could not abuse this information to
   *   determine the stored value.
   *
   * @internal
   *
   * @see \Drupal\rest\Plugin\rest\resource\EntityResource::checkPatchFieldAccess()
   */
  protected function checkPatchFieldAccess(FieldItemListInterface $original_field, FieldItemListInterface $received_field) {

    // If the user is allowed to edit the field, it is always safe to set the
    // received value. We may be setting an unchanged value, but that is ok.
    $field_edit_access = $original_field
      ->access('edit', NULL, TRUE);
    if ($field_edit_access
      ->isAllowed()) {
      return TRUE;
    }

    // The user might not have access to edit the field, but still needs to
    // submit the current field value as part of the PATCH request. For
    // example, the entity keys required by denormalizers. Therefore, if the
    // received value equals the stored value, return FALSE without throwing an
    // exception. But only for fields that the user has access to view, because
    // the user has no legitimate way of knowing the current value of fields
    // that they are not allowed to view, and we must not make the presence or
    // absence of a 403 response a way to find that out.
    if ($original_field
      ->access('view') && $original_field
      ->equals($received_field)) {
      return FALSE;
    }

    // It's helpful and safe to let the user know when they are not allowed to
    // update a field.
    $field_name = $received_field
      ->getName();
    throw new EntityAccessDeniedHttpException($original_field
      ->getEntity(), $field_edit_access, '/data/attributes/' . $field_name, sprintf('The current user is not allowed to PATCH the selected field (%s).', $field_name));
  }

  /**
   * Build a collection of the entities to respond with and access objects.
   *
   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
   *   The entity storage to load the entities from.
   * @param int[] $ids
   *   An array of entity IDs, keyed by revision ID if the entity type is
   *   revisionable.
   * @param bool $load_latest_revisions
   *   Whether to load the latest revisions instead of the defaults.
   *
   * @return array
   *   An array of loaded entities and/or an access exceptions.
   */
  protected function loadEntitiesWithAccess(EntityStorageInterface $storage, array $ids, $load_latest_revisions) {
    $output = [];
    if ($load_latest_revisions) {
      assert($storage instanceof RevisionableStorageInterface);
      $entities = $storage
        ->loadMultipleRevisions(array_keys($ids));
    }
    else {
      $entities = $storage
        ->loadMultiple($ids);
    }
    foreach ($entities as $entity) {
      $output[$entity
        ->id()] = $this->entityAccessChecker
        ->getAccessCheckedResourceObject($entity);
    }
    return array_values($output);
  }

  /**
   * Checks if the given entity exists.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity for which to test existence.
   *
   * @return bool
   *   Whether the entity already has been created.
   */
  protected function entityExists(EntityInterface $entity) {
    $entity_storage = $this->entityTypeManager
      ->getStorage($entity
      ->getEntityTypeId());
    return !empty($entity_storage
      ->loadByProperties([
      'uuid' => $entity
        ->uuid(),
    ]));
  }

  /**
   * Extracts JSON:API query parameters from the request.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
   *   The JSON:API resource type.
   *
   * @return array
   *   An array of JSON:API parameters like `sort` and `filter`.
   */
  protected function getJsonApiParams(Request $request, ResourceType $resource_type) {
    if ($request->query
      ->has('filter')) {
      $params[Filter::KEY_NAME] = Filter::createFromQueryParameter($request->query
        ->all('filter'), $resource_type, $this->fieldResolver);
    }
    if ($request->query
      ->has('sort')) {
      $params[Sort::KEY_NAME] = Sort::createFromQueryParameter($request->query
        ->all()['sort']);
    }
    if ($request->query
      ->has('page')) {
      $params[OffsetPage::KEY_NAME] = OffsetPage::createFromQueryParameter($request->query
        ->all('page'));
    }
    else {
      $params[OffsetPage::KEY_NAME] = OffsetPage::createFromQueryParameter([
        'page' => [
          'offset' => OffsetPage::DEFAULT_OFFSET,
          'limit' => OffsetPage::SIZE_MAX,
        ],
      ]);
    }
    return $params;
  }

  /**
   * Get the full URL for a given request object.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   * @param array|null $query
   *   The query parameters to use. Leave it empty to get the query from the
   *   request object.
   *
   * @return \Drupal\Core\Url
   *   The full URL.
   */
  protected static function getRequestLink(Request $request, $query = NULL) {
    if ($query === NULL) {
      return Url::fromUri($request
        ->getUri());
    }
    $uri_without_query_string = $request
      ->getSchemeAndHttpHost() . $request
      ->getBaseUrl() . $request
      ->getPathInfo();
    return Url::fromUri($uri_without_query_string)
      ->setOption('query', $query);
  }

  /**
   * Get the pager links for a given request object.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   * @param \Drupal\jsonapi\Query\OffsetPage $page_param
   *   The current pagination parameter for the requested collection.
   * @param array $link_context
   *   An associative array with extra data to build the links.
   *
   * @return \Drupal\jsonapi\JsonApiResource\LinkCollection
   *   A LinkCollection, with:
   *   - a 'next' key if it is not the last page;
   *   - 'prev' and 'first' keys if it's not the first page.
   */
  protected static function getPagerLinks(Request $request, OffsetPage $page_param, array $link_context = []) {
    $pager_links = new LinkCollection([]);
    if (!empty($link_context['total_count']) && !($total = (int) $link_context['total_count'])) {
      return $pager_links;
    }
    $offset = $page_param
      ->getOffset();
    $size = $page_param
      ->getSize();
    if ($size <= 0) {
      $cacheability = (new CacheableMetadata())
        ->addCacheContexts([
        'url.query_args:page',
      ]);
      throw new CacheableBadRequestHttpException($cacheability, sprintf('The page size needs to be a positive integer.'));
    }
    $query = (array) $request->query
      ->getIterator();

    // Check if this is not the last page.
    if ($link_context['has_next_page']) {
      $next_url = static::getRequestLink($request, static::getPagerQueries('next', $offset, $size, $query));
      $pager_links = $pager_links
        ->withLink('next', new Link(new CacheableMetadata(), $next_url, 'next'));
      if (!empty($total)) {
        $last_url = static::getRequestLink($request, static::getPagerQueries('last', $offset, $size, $query, $total));
        $pager_links = $pager_links
          ->withLink('last', new Link(new CacheableMetadata(), $last_url, 'last'));
      }
    }

    // Check if this is not the first page.
    if ($offset > 0) {
      $first_url = static::getRequestLink($request, static::getPagerQueries('first', $offset, $size, $query));
      $pager_links = $pager_links
        ->withLink('first', new Link(new CacheableMetadata(), $first_url, 'first'));
      $prev_url = static::getRequestLink($request, static::getPagerQueries('prev', $offset, $size, $query));
      $pager_links = $pager_links
        ->withLink('prev', new Link(new CacheableMetadata(), $prev_url, 'prev'));
    }
    return $pager_links;
  }

  /**
   * Get the query param array.
   *
   * @param string $link_id
   *   The name of the pagination link requested.
   * @param int $offset
   *   The starting index.
   * @param int $size
   *   The pagination page size.
   * @param array $query
   *   The query parameters.
   * @param int $total
   *   The total size of the collection.
   *
   * @return array
   *   The pagination query param array.
   */
  protected static function getPagerQueries($link_id, $offset, $size, array $query = [], $total = 0) {
    $extra_query = [];
    switch ($link_id) {
      case 'next':
        $extra_query = [
          'page' => [
            'offset' => $offset + $size,
            'limit' => $size,
          ],
        ];
        break;
      case 'first':
        $extra_query = [
          'page' => [
            'offset' => 0,
            'limit' => $size,
          ],
        ];
        break;
      case 'last':
        if ($total) {
          $extra_query = [
            'page' => [
              'offset' => (ceil($total / $size) - 1) * $size,
              'limit' => $size,
            ],
          ];
        }
        break;
      case 'prev':
        $extra_query = [
          'page' => [
            'offset' => max($offset - $size, 0),
            'limit' => $size,
          ],
        ];
        break;
    }
    return array_merge($query, $extra_query);
  }

}

Members