You are here

public function FieldResolver::resolveInternalEntityQueryPath in JSON:API 8

Same name and namespace in other branches
  1. 8.2 src/Context/FieldResolver.php \Drupal\jsonapi\Context\FieldResolver::resolveInternalEntityQueryPath()

Resolves external field expressions into entity query compatible paths.

It is often required to reference data which may exist across a relationship. For example, you may want to sort a list of articles by a field on the article author's representative entity. Or you may wish to filter a list of content by the name of referenced taxonomy terms.

In an effort to simplify the referenced paths and align them with the structure of JSON API responses and the structure of the hypothetical "reference document" (see link), it is possible to alias field names and elide the "entity" keyword from them (this word is used by the entity query system to traverse entity references).

This method takes this external field expression and and attempts to resolve any aliases and/or abbreviations into a field expression that will be compatible with the entity query system.

@link http://jsonapi.org/recommendations/#urls-reference-document

Example: 'uid.field_first_name' -> 'uid.entity.field_first_name'. 'author.firstName' -> 'field_author.entity.field_first_name'

Parameters

string $entity_type_id: The type of the entity for which to resolve the field name.

string $bundle: The bundle of the entity for which to resolve the field name.

string $external_field_name: The public field name to map to a Drupal field name.

Return value

string The mapped field name.

Throws

\Symfony\Component\HttpKernel\Exception\BadRequestHttpException

File

src/Context/FieldResolver.php, line 252

Class

FieldResolver
A service that evaluates external path expressions against Drupal fields.

Namespace

Drupal\jsonapi\Context

Code

public function resolveInternalEntityQueryPath($entity_type_id, $bundle, $external_field_name) {
  $resource_type = $this->resourceTypeRepository
    ->get($entity_type_id, $bundle);
  if (empty($external_field_name)) {
    throw new BadRequestHttpException('No field name was provided for the filter.');
  }

  // Turns 'uid.categories.name' into
  // 'uid.entity.field_category.entity.name'. This may be too simple, but it
  // works for the time being.
  $parts = explode('.', $external_field_name);
  $unresolved_path_parts = $parts;
  $reference_breadcrumbs = [];

  /* @var \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types */
  $resource_types = [
    $resource_type,
  ];

  // This complex expression is needed to handle the string, "0", which would
  // otherwise be evaluated as FALSE.
  while (!is_null($part = array_shift($parts))) {
    $field_name = $this
      ->getInternalName($part, $resource_types);

    // If none of the resource types are traversable, assume that the
    // remaining path parts are targeting field deltas and/or field
    // properties.
    if (!$this
      ->resourceTypesAreTraversable($resource_types)) {
      $reference_breadcrumbs[] = $field_name;
      return $this
        ->constructInternalPath($reference_breadcrumbs, $parts);
    }

    // Different resource types have different field definitions.
    $candidate_definitions = $this
      ->getFieldItemDefinitions($resource_types, $field_name);

    // If there are no definitions, then the field does not exist.
    if (empty($candidate_definitions)) {
      throw new BadRequestHttpException(sprintf('Invalid nested filtering. The field `%s`, given in the path `%s`, does not exist.', $part, $external_field_name));
    }

    // We have a valid field, so add it to the validated trail of path parts.
    $reference_breadcrumbs[] = $field_name;

    // Remove resource types which do not have a candidate definition.
    $resource_types = array_filter($resource_types, function (ResourceType $resource_type) use ($candidate_definitions) {
      return isset($candidate_definitions[$resource_type
        ->getTypeName()]);
    });

    // Check access to execute a query for each field per resource type since
    // field definitions are bundle-specific.
    foreach ($resource_types as $resource_type) {
      $field_access = $this
        ->getFieldAccess($resource_type, $field_name);
      if (!$field_access
        ->isAllowed()) {
        $message = sprintf('The current user is not authorized to filter by the `%s` field, given in the path `%s`.', $field_name, implode('.', $reference_breadcrumbs));
        if ($field_access instanceof AccessResultReasonInterface && ($reason = $field_access
          ->getReason()) && !empty($reason)) {
          $message .= ' ' . $reason;
        }
        throw new AccessDeniedHttpException($message);
      }
    }

    // Get all of the referenceable resource types.
    $resource_types = $this
      ->getReferenceableResourceTypes($candidate_definitions);

    // If there are no remaining path parts, the process is finished.
    if (empty($parts)) {
      return $this
        ->constructInternalPath($reference_breadcrumbs);
    }

    // If the next part is a delta, as in "body.0.value", then we add it to
    // the breadcrumbs and remove it from the parts that still must be
    // processed.
    if (static::isDelta($parts[0])) {
      $reference_breadcrumbs[] = array_shift($parts);
    }

    // If there are no remaining path parts, the process is finished.
    if (empty($parts)) {
      return $this
        ->constructInternalPath($reference_breadcrumbs);
    }

    // Determine if the next part is not a property of $field_name.
    if (!static::isCandidateDefinitionProperty($parts[0], $candidate_definitions)) {

      // The next path part is neither a delta nor a field property, so it
      // must be a field on a targeted resource type. We need to guess the
      // intermediate reference property since one was not provided.
      //
      // For example, the path `uid.name` for a `node--article` resource type
      // will be resolved into `uid.entity.name`.
      $reference_breadcrumbs[] = static::getDataReferencePropertyName($candidate_definitions, $parts, $unresolved_path_parts);
    }
    else {

      // If the property is not a reference property, then all
      // remaining parts must be further property specifiers.
      // @todo: to provide a better DX, we should actually validate that the
      // remaining parts are in fact valid properties.
      if (!static::isCandidateDefinitionReferenceProperty($parts[0], $candidate_definitions)) {
        return $this
          ->constructInternalPath($reference_breadcrumbs, $parts);
      }

      // The property is a reference, so add it to the breadcrumbs and
      // continue resolving fields.
      $reference_breadcrumbs[] = array_shift($parts);
    }
  }

  // Reconstruct the full path to the final reference field.
  return $this
    ->constructInternalPath($reference_breadcrumbs);
}