You are here

class ResourceIdentifier in Drupal 10

Same name and namespace in other branches
  1. 8 core/modules/jsonapi/src/JsonApiResource/ResourceIdentifier.php \Drupal\jsonapi\JsonApiResource\ResourceIdentifier
  2. 9 core/modules/jsonapi/src/JsonApiResource/ResourceIdentifier.php \Drupal\jsonapi\JsonApiResource\ResourceIdentifier

Represents a JSON:API resource identifier object.

The official JSON:API JSON-Schema document requires that no two resource identifier objects are duplicates, however Drupal allows multiple entity reference items to the same entity. Here, these are termed "parallel" relationships (as in "parallel edges" of a graph).

This class adds a concept of an

arity;

member under each its

meta;

object. The value of this member is an integer that is incremented by 1 (starting from 0) for each repeated resource identifier sharing a common

type;

and

id;

.

There are a number of helper methods to process the logic of dealing with resource identifies with and without arity.

@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\JsonApiResource\ResourceIdentifier implements \Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface

Expanded class hierarchy of ResourceIdentifier

See also

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

jsonapi.api.php

http://jsonapi.org/format/#document-resource-object-relationships

https://github.com/json-api/json-api/pull/1156#issuecomment-325377995

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

3 files declare their use of ResourceIdentifier
EntityAccessDeniedHttpException.php in core/modules/jsonapi/src/Exception/EntityAccessDeniedHttpException.php
EntityResource.php in core/modules/jsonapi/src/Controller/EntityResource.php
ResourceIdentifierNormalizerTest.php in core/modules/jsonapi/tests/src/Unit/Normalizer/ResourceIdentifierNormalizerTest.php

File

core/modules/jsonapi/src/JsonApiResource/ResourceIdentifier.php, line 38

Namespace

Drupal\jsonapi\JsonApiResource
View source
class ResourceIdentifier implements ResourceIdentifierInterface {
  const ARITY_KEY = 'arity';

  /**
   * The JSON:API resource type name.
   *
   * @var string
   */
  protected $resourceTypeName;

  /**
   * The JSON:API resource type.
   *
   * @var \Drupal\jsonapi\ResourceType\ResourceType
   */
  protected $resourceType;

  /**
   * The resource ID.
   *
   * @var string
   */
  protected $id;

  /**
   * The relationship's metadata.
   *
   * @var array
   */
  protected $meta;

  /**
   * ResourceIdentifier constructor.
   *
   * @param \Drupal\jsonapi\ResourceType\ResourceType|string $resource_type
   *   The JSON:API resource type or a JSON:API resource type name.
   * @param string $id
   *   The resource ID.
   * @param array $meta
   *   Any metadata for the ResourceIdentifier.
   */
  public function __construct($resource_type, $id, array $meta = []) {
    assert(is_string($resource_type) || $resource_type instanceof ResourceType);
    assert(!isset($meta[static::ARITY_KEY]) || is_int($meta[static::ARITY_KEY]) && $meta[static::ARITY_KEY] >= 0);
    $this->resourceTypeName = is_string($resource_type) ? $resource_type : $resource_type
      ->getTypeName();
    $this->id = $id;
    $this->meta = $meta;
    if (!is_string($resource_type)) {
      $this->resourceType = $resource_type;
    }
  }

  /**
   * Gets the ResourceIdentifier's JSON:API resource type name.
   *
   * @return string
   *   The JSON:API resource type name.
   */
  public function getTypeName() {
    return $this->resourceTypeName;
  }

  /**
   * {@inheritdoc}
   */
  public function getResourceType() {
    if (!isset($this->resourceType)) {

      /** @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository */
      $resource_type_repository = \Drupal::service('jsonapi.resource_type.repository');
      $this->resourceType = $resource_type_repository
        ->getByTypeName($this
        ->getTypeName());
    }
    return $this->resourceType;
  }

  /**
   * Gets the ResourceIdentifier's ID.
   *
   * @return string
   *   The ID.
   */
  public function getId() {
    return $this->id;
  }

  /**
   * Whether this ResourceIdentifier has an arity.
   *
   * @return int
   *   TRUE if the ResourceIdentifier has an arity, FALSE otherwise.
   */
  public function hasArity() {
    return isset($this->meta[static::ARITY_KEY]);
  }

  /**
   * Gets the ResourceIdentifier's arity.
   *
   * One must check self::hasArity() before calling this method.
   *
   * @return int
   *   The arity.
   */
  public function getArity() {
    assert($this
      ->hasArity());
    return $this->meta[static::ARITY_KEY];
  }

  /**
   * Returns a copy of the given ResourceIdentifier with the given arity.
   *
   * @param int $arity
   *   The new arity; must be a non-negative integer.
   *
   * @return static
   *   A newly created ResourceIdentifier with the given arity, otherwise
   *   the same.
   */
  public function withArity($arity) {
    return new static($this
      ->getResourceType(), $this
      ->getId(), [
      static::ARITY_KEY => $arity,
    ] + $this
      ->getMeta());
  }

  /**
   * Gets the resource identifier objects metadata.
   *
   * @return array
   *   The metadata.
   */
  public function getMeta() {
    return $this->meta;
  }

  /**
   * Determines if two ResourceIdentifiers are the same.
   *
   * This method does not consider parallel relationships with different arity
   * values to be duplicates. For that, use the isParallel() method.
   *
   * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $a
   *   The first ResourceIdentifier object.
   * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $b
   *   The second ResourceIdentifier object.
   *
   * @return bool
   *   TRUE if both relationships reference the same resource and do not have
   *   two distinct arity's, FALSE otherwise.
   *
   *   For example, if $a and $b both reference the same resource identifier,
   *   they can only be distinct if they *both* have an arity and those values
   *   are not the same. If $a or $b does not have an arity, they will be
   *   considered duplicates.
   */
  public static function isDuplicate(ResourceIdentifier $a, ResourceIdentifier $b) {
    return static::compare($a, $b) === 0;
  }

  /**
   * Determines if two ResourceIdentifiers identify the same resource object.
   *
   * This method does not consider arity.
   *
   * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $a
   *   The first ResourceIdentifier object.
   * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $b
   *   The second ResourceIdentifier object.
   *
   * @return bool
   *   TRUE if both relationships reference the same resource, even when they
   *   have differing arity values, FALSE otherwise.
   */
  public static function isParallel(ResourceIdentifier $a, ResourceIdentifier $b) {
    return static::compare($a
      ->withArity(0), $b
      ->withArity(0)) === 0;
  }

  /**
   * Compares ResourceIdentifier objects.
   *
   * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $a
   *   The first ResourceIdentifier object.
   * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $b
   *   The second ResourceIdentifier object.
   *
   * @return int
   *   Returns 0 if $a and $b are duplicate ResourceIdentifiers. If $a and $b
   *   identify the same resource but have distinct arity values, then the
   *   return value will be arity $a minus arity $b. -1 otherwise.
   */
  public static function compare(ResourceIdentifier $a, ResourceIdentifier $b) {
    $result = strcmp(sprintf('%s:%s', $a
      ->getTypeName(), $a
      ->getId()), sprintf('%s:%s', $b
      ->getTypeName(), $b
      ->getId()));

    // If type and ID do not match, return their ordering.
    if ($result !== 0) {
      return $result;
    }

    // If both $a and $b have an arity, then return the order by arity.
    // Otherwise, they are considered equal.
    return $a
      ->hasArity() && $b
      ->hasArity() ? $a
      ->getArity() - $b
      ->getArity() : 0;
  }

  /**
   * Deduplicates an array of ResourceIdentifier objects.
   *
   * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier[] $resource_identifiers
   *   The list of ResourceIdentifiers to deduplicate.
   *
   * @return \Drupal\jsonapi\JsonApiResource\ResourceIdentifier[]
   *   A deduplicated array of ResourceIdentifier objects.
   *
   * @see self::isDuplicate()
   */
  public static function deduplicate(array $resource_identifiers) {
    return array_reduce(array_slice($resource_identifiers, 1), function ($deduplicated, $current) {
      assert($current instanceof static);
      return array_merge($deduplicated, array_reduce($deduplicated, function ($duplicate, $previous) use ($current) {
        return $duplicate ?: static::isDuplicate($previous, $current);
      }, FALSE) ? [] : [
        $current,
      ]);
    }, array_slice($resource_identifiers, 0, 1));
  }

  /**
   * Determines if an array of ResourceIdentifier objects is duplicate free.
   *
   * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier[] $resource_identifiers
   *   The list of ResourceIdentifiers to assess.
   *
   * @return bool
   *   Whether all the given resource identifiers are unique.
   */
  public static function areResourceIdentifiersUnique(array $resource_identifiers) {
    return count($resource_identifiers) === count(static::deduplicate($resource_identifiers));
  }

  /**
   * Creates a ResourceIdentifier object.
   *
   * @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item
   *   The entity reference field item from which to create the relationship.
   * @param int $arity
   *   (optional) The arity of the relationship.
   *
   * @return self
   *   A new ResourceIdentifier object.
   */
  public static function toResourceIdentifier(EntityReferenceItem $item, $arity = NULL) {
    $property_name = static::getDataReferencePropertyName($item);
    $target = $item
      ->get($property_name)
      ->getValue();
    if ($target === NULL) {
      return static::getVirtualOrMissingResourceIdentifier($item);
    }
    assert($target instanceof EntityInterface);

    /** @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository */
    $resource_type_repository = \Drupal::service('jsonapi.resource_type.repository');
    $resource_type = $resource_type_repository
      ->get($target
      ->getEntityTypeId(), $target
      ->bundle());

    // Remove unwanted properties from the meta value, usually 'entity'
    // and 'target_id'.
    $properties = TypedDataInternalPropertiesHelper::getNonInternalProperties($item);
    $main_property_name = $item
      ->getDataDefinition()
      ->getMainPropertyName();
    $meta = array_diff_key($properties, array_flip([
      $property_name,
      $main_property_name,
    ]));
    if (!is_null($arity)) {
      $meta[static::ARITY_KEY] = $arity;
    }
    $meta["drupal_internal__{$main_property_name}"] = $properties[$main_property_name];
    return new static($resource_type, $target
      ->uuid(), $meta);
  }

  /**
   * Creates an array of ResourceIdentifier objects.
   *
   * @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items
   *   The entity reference field items from which to create the relationship
   *   array.
   *
   * @return self[]
   *   An array of new ResourceIdentifier objects with appropriate arity values.
   */
  public static function toResourceIdentifiers(EntityReferenceFieldItemListInterface $items) {
    $relationships = [];
    foreach ($items
      ->filterEmptyItems() as $item) {

      // Create a ResourceIdentifier from the field item. This will make it
      // comparable with all previous field items. Here, it is assumed that the
      // resource identifier is unique so it has no arity. If a parallel
      // relationship is encountered, it will be assigned later.
      $relationship = static::toResourceIdentifier($item);
      if ($relationship
        ->getResourceType()
        ->isInternal()) {
        continue;
      }

      // Now, iterate over the previously seen resource identifiers in reverse
      // order. Reverse order is important so that when a parallel relationship
      // is encountered, it will have the highest arity value so the current
      // relationship's arity value can simply be incremented by one.

      /** @var \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $existing */
      foreach (array_reverse($relationships, TRUE) as $index => $existing) {
        $is_parallel = static::isParallel($existing, $relationship);
        if ($is_parallel) {

          // A parallel relationship has been found. If the previous
          // relationship does not have an arity, it must now be assigned an
          // arity of 0.
          if (!$existing
            ->hasArity()) {
            $relationships[$index] = $existing
              ->withArity(0);
          }

          // Since the new ResourceIdentifier is parallel, it must have an arity
          // assigned to it that is the arity of the last parallel
          // relationship's arity + 1.
          $relationship = $relationship
            ->withArity($relationships[$index]
            ->getArity() + 1);
          break;
        }
      }

      // Finally, append the relationship to the list of ResourceIdentifiers.
      $relationships[] = $relationship;
    }
    return $relationships;
  }

  /**
   * Creates an array of ResourceIdentifier objects with arity on every value.
   *
   * @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items
   *   The entity reference field items from which to create the relationship
   *   array.
   *
   * @return self[]
   *   An array of new ResourceIdentifier objects with appropriate arity values.
   *   Unlike self::toResourceIdentifiers(), this method does not omit arity
   *   when an identifier is not parallel to any other identifier.
   */
  public static function toResourceIdentifiersWithArityRequired(EntityReferenceFieldItemListInterface $items) {
    return array_map(function (ResourceIdentifier $identifier) {
      return $identifier
        ->hasArity() ? $identifier : $identifier
        ->withArity(0);
    }, static::toResourceIdentifiers($items));
  }

  /**
   * Creates a ResourceIdentifier object.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity from which to create the resource identifier.
   *
   * @return self
   *   A new ResourceIdentifier object.
   */
  public static function fromEntity(EntityInterface $entity) {

    /** @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository */
    $resource_type_repository = \Drupal::service('jsonapi.resource_type.repository');
    $resource_type = $resource_type_repository
      ->get($entity
      ->getEntityTypeId(), $entity
      ->bundle());
    return new static($resource_type, $entity
      ->uuid());
  }

  /**
   * Helper method to determine which field item property contains an entity.
   *
   * @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item
   *   The entity reference item for which to determine the entity property
   *   name.
   *
   * @return string
   *   The property name which has an entity as its value.
   */
  protected static function getDataReferencePropertyName(EntityReferenceItem $item) {
    foreach ($item
      ->getDataDefinition()
      ->getPropertyDefinitions() as $property_name => $property_definition) {
      if ($property_definition instanceof DataReferenceDefinitionInterface) {
        return $property_name;
      }
    }
  }

  /**
   * Creates a ResourceIdentifier for a NULL or FALSE entity reference item.
   *
   * @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item
   *   The entity reference field item.
   *
   * @return self
   *   A new ResourceIdentifier object.
   */
  protected static function getVirtualOrMissingResourceIdentifier(EntityReferenceItem $item) {
    $resource_type_repository = \Drupal::service('jsonapi.resource_type.repository');
    $property_name = static::getDataReferencePropertyName($item);
    $value = $item
      ->get($property_name)
      ->getValue();
    assert($value === NULL);
    $field = $item
      ->getParent();
    assert($field instanceof EntityReferenceFieldItemListInterface);
    $host_entity = $field
      ->getEntity();
    assert($host_entity instanceof EntityInterface);
    $resource_type = $resource_type_repository
      ->get($host_entity
      ->getEntityTypeId(), $host_entity
      ->bundle());
    assert($resource_type instanceof ResourceType);
    $relatable_resource_types = $resource_type
      ->getRelatableResourceTypesByField($resource_type
      ->getPublicName($field
      ->getName()));
    assert(!empty($relatable_resource_types));
    $get_metadata = function ($type) {
      return [
        'links' => [
          'help' => [
            'href' => "https://www.drupal.org/docs/8/modules/json-api/core-concepts#{$type}",
            'meta' => [
              'about' => "Usage and meaning of the '{$type}' resource identifier.",
            ],
          ],
        ],
      ];
    };
    $resource_type = reset($relatable_resource_types);

    // A non-empty entity reference field that refers to a non-existent entity
    // is not a data integrity problem. For example, Term entities' "parent"
    // entity reference field uses target_id zero to refer to the non-existent
    // "<root>" term. And references to entities that no longer exist are not
    // cleaned up by Drupal; hence we map it to a "missing" resource.
    if ($field
      ->getFieldDefinition()
      ->getSetting('target_type') === 'taxonomy_term' && $item
      ->get('target_id')
      ->getCastedValue() === 0) {
      if (count($relatable_resource_types) !== 1) {
        throw new \RuntimeException('Relationships to virtual resources are possible only if a single resource type is relatable.');
      }
      return new static($resource_type, 'virtual', $get_metadata('virtual'));
    }
    else {

      // In case of a dangling reference, it is impossible to determine which
      // resource type it used to reference, because that requires knowing the
      // referenced bundle, which Drupal does not store.
      // If we can reliably determine the resource type of the dangling
      // reference, use it; otherwise conjure a fake resource type out of thin
      // air, one that indicates we don't know the bundle.
      $resource_type = count($relatable_resource_types) > 1 ? new ResourceType('?', '?', '') : reset($relatable_resource_types);
      return new static($resource_type, 'missing', $get_metadata('missing'));
    }
  }

}

Members