Mercurial > hg > cmmr2012-drupal-site
diff core/modules/jsonapi/src/JsonApiResource/ResourceIdentifier.php @ 5:12f9dff5fda9 tip
Update to Drupal core 8.7.1
author | Chris Cannam |
---|---|
date | Thu, 09 May 2019 15:34:47 +0100 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/core/modules/jsonapi/src/JsonApiResource/ResourceIdentifier.php Thu May 09 15:34:47 2019 +0100 @@ -0,0 +1,458 @@ +<?php + +namespace Drupal\jsonapi\JsonApiResource; + +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Field\EntityReferenceFieldItemListInterface; +use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; +use Drupal\Core\TypedData\DataReferenceDefinitionInterface; +use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper; +use Drupal\jsonapi\ResourceType\ResourceType; + +/** + * 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 @code arity @endcode member under each its + * @code meta @endcode 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 @code type @endcode and @code id @endcode. + * + * 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. + * + * @see https://www.drupal.org/project/jsonapi/issues/3032787 + * @see jsonapi.api.php + * + * @see http://jsonapi.org/format/#document-resource-object-relationships + * @see https://github.com/json-api/json-api/pull/1156#issuecomment-325377995 + * @see https://www.drupal.org/project/jsonapi/issues/2864680 + */ +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); + $meta = array_diff_key($properties, array_flip([$property_name, $item->getDataDefinition()->getMainPropertyName()])); + if (!is_null($arity)) { + $meta[static::ARITY_KEY] = $arity; + } + 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 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); + // 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 self $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($field->getName()); + $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')); + } + } + +}