You are here

public function FieldResolver::resolveInternalEntityQueryPath in Drupal 9

Same name and namespace in other branches
  1. 8 core/modules/jsonapi/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 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

\Drupal\jsonapi\ResourceType\ResourceType $resource_type: The JSON:API resource type from which to resolve the field name.

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

string $operator: (optional) The operator of the condition for which the path should be resolved.

Return value

string The mapped field name.

Throws

\Drupal\Core\Http\Exception\CacheableBadRequestHttpException

File

core/modules/jsonapi/src/Context/FieldResolver.php, line 284

Class

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

Namespace

Drupal\jsonapi\Context

Code

public function resolveInternalEntityQueryPath(ResourceType $resource_type, $external_field_name, $operator = NULL) {
  $cacheability = (new CacheableMetadata())
    ->addCacheContexts([
    'url.query_args:filter',
    'url.query_args:sort',
  ]);
  if (empty($external_field_name)) {
    throw new CacheableBadRequestHttpException($cacheability, '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))) {
    if (!$this
      ->isMemberFilterable($part, $resource_types)) {
      throw new CacheableBadRequestHttpException($cacheability, sprintf('Invalid nested filtering. The field `%s`, given in the path `%s`, does not exist.', $part, $external_field_name));
    }
    $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);
    assert(!empty($candidate_definitions));

    // 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);
      $cacheability
        ->addCacheableDependency($field_access);
      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 CacheableAccessDeniedHttpException($cacheability, $message);
      }
    }

    // Get all of the referenceable resource types.
    $resource_types = $this
      ->getRelatableResourceTypes($resource_types, $candidate_definitions);
    $at_least_one_entity_reference_field = FALSE;
    $candidate_property_names = array_unique(NestedArray::mergeDeepArray(array_map(function (FieldItemDataDefinitionInterface $definition) use (&$at_least_one_entity_reference_field) {
      $property_definitions = $definition
        ->getPropertyDefinitions();
      return array_reduce(array_keys($property_definitions), function ($property_names, $property_name) use ($property_definitions, &$at_least_one_entity_reference_field) {
        $property_definition = $property_definitions[$property_name];
        $is_data_reference_definition = $property_definition instanceof DataReferenceTargetDefinition;
        if (!$property_definition
          ->isInternal()) {

          // Entity reference fields are special: their reference property
          // (usually `target_id`) is exposed in the JSON:API representation
          // with a prefix.
          $property_names[] = $is_data_reference_definition ? 'id' : $property_name;
        }
        if ($is_data_reference_definition) {
          $at_least_one_entity_reference_field = TRUE;
          $property_names[] = "drupal_internal__{$property_name}";
        }
        return $property_names;
      }, []);
    }, $candidate_definitions)));

    // Determine if the specified field has one property or many in its
    // JSON:API representation, or if it is an relationship (an entity
    // reference field), in which case the `id` of the related resource must
    // always be specified.
    $property_specifier_needed = $at_least_one_entity_reference_field || count($candidate_property_names) > 1;

    // If there are no remaining path parts, the process is finished unless
    // the field has multiple properties, in which case one must be specified.
    if (empty($parts)) {

      // If the operator is asserting the presence or absence of a
      // relationship entirely, it does not make sense to require a property
      // specifier.
      if ($property_specifier_needed && (!$at_least_one_entity_reference_field || !in_array($operator, [
        'IS NULL',
        'IS NOT NULL',
      ], TRUE))) {
        $possible_specifiers = array_map(function ($specifier) use ($at_least_one_entity_reference_field) {
          return $at_least_one_entity_reference_field && $specifier !== 'id' ? "meta.{$specifier}" : $specifier;
        }, $candidate_property_names);
        throw new CacheableBadRequestHttpException($cacheability, sprintf('Invalid nested filtering. The field `%s`, given in the path `%s` is incomplete, it must end with one of the following specifiers: `%s`.', $part, $external_field_name, implode('`, `', $possible_specifiers)));
      }
      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);
    }

    // JSON:API outputs entity reference field properties under a meta object
    // on a relationship. If the filter specifies one of these properties, it
    // must prefix the property name with `meta`. The only exception is if the
    // next path part is the same as the name for the reference property
    // (typically `entity`), this is permitted to disambiguate the case of a
    // field name on the target entity which is the same a property name on
    // the entity reference field.
    if ($at_least_one_entity_reference_field && $parts[0] !== 'id') {
      if ($parts[0] === 'meta') {
        array_shift($parts);
      }
      elseif (in_array($parts[0], $candidate_property_names) && !static::isCandidateDefinitionReferenceProperty($parts[0], $candidate_definitions)) {
        throw new CacheableBadRequestHttpException($cacheability, sprintf('Invalid nested filtering. The property `%s`, given in the path `%s` belongs to the meta object of a relationship and must be preceded by `meta`.', $parts[0], $external_field_name));
      }
    }

    // Determine if the next part is not a property of $field_name.
    if (!static::isCandidateDefinitionProperty($parts[0], $candidate_definitions) && !empty(static::getAllDataReferencePropertyNames($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.
      if (!static::isCandidateDefinitionReferenceProperty($parts[0], $candidate_definitions)) {

        // If a field property is specified on a field with only one property
        // defined, throw an error because in the JSON:API output, it does not
        // exist. This is because JSON:API elides single-value properties;
        // respecting it would leak this Drupalism out.
        if (count($candidate_property_names) === 1) {
          throw new CacheableBadRequestHttpException($cacheability, sprintf('Invalid nested filtering. The property `%s`, given in the path `%s`, does not exist. Filter by `%s`, not `%s` (the JSON:API module elides property names from single-property fields).', $parts[0], $external_field_name, substr($external_field_name, 0, strlen($external_field_name) - strlen($parts[0]) - 1), $external_field_name));
        }
        elseif (!in_array($parts[0], $candidate_property_names, TRUE)) {
          throw new CacheableBadRequestHttpException($cacheability, sprintf('Invalid nested filtering. The property `%s`, given in the path `%s`, does not exist. Must be one of the following property names: `%s`.', $parts[0], $external_field_name, implode('`, `', $candidate_property_names)));
        }
        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);
}