Chris@18: = 0); Chris@18: $this->resourceTypeName = is_string($resource_type) ? $resource_type : $resource_type->getTypeName(); Chris@18: $this->id = $id; Chris@18: $this->meta = $meta; Chris@18: if (!is_string($resource_type)) { Chris@18: $this->resourceType = $resource_type; Chris@18: } Chris@18: } Chris@18: Chris@18: /** Chris@18: * Gets the ResourceIdentifier's JSON:API resource type name. Chris@18: * Chris@18: * @return string Chris@18: * The JSON:API resource type name. Chris@18: */ Chris@18: public function getTypeName() { Chris@18: return $this->resourceTypeName; Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: public function getResourceType() { Chris@18: if (!isset($this->resourceType)) { Chris@18: /* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository */ Chris@18: $resource_type_repository = \Drupal::service('jsonapi.resource_type.repository'); Chris@18: $this->resourceType = $resource_type_repository->getByTypeName($this->getTypeName()); Chris@18: } Chris@18: return $this->resourceType; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Gets the ResourceIdentifier's ID. Chris@18: * Chris@18: * @return string Chris@18: * The ID. Chris@18: */ Chris@18: public function getId() { Chris@18: return $this->id; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Whether this ResourceIdentifier has an arity. Chris@18: * Chris@18: * @return int Chris@18: * TRUE if the ResourceIdentifier has an arity, FALSE otherwise. Chris@18: */ Chris@18: public function hasArity() { Chris@18: return isset($this->meta[static::ARITY_KEY]); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Gets the ResourceIdentifier's arity. Chris@18: * Chris@18: * One must check self::hasArity() before calling this method. Chris@18: * Chris@18: * @return int Chris@18: * The arity. Chris@18: */ Chris@18: public function getArity() { Chris@18: assert($this->hasArity()); Chris@18: return $this->meta[static::ARITY_KEY]; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Returns a copy of the given ResourceIdentifier with the given arity. Chris@18: * Chris@18: * @param int $arity Chris@18: * The new arity; must be a non-negative integer. Chris@18: * Chris@18: * @return static Chris@18: * A newly created ResourceIdentifier with the given arity, otherwise Chris@18: * the same. Chris@18: */ Chris@18: public function withArity($arity) { Chris@18: return new static($this->getResourceType(), $this->getId(), [static::ARITY_KEY => $arity] + $this->getMeta()); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Gets the resource identifier objects metadata. Chris@18: * Chris@18: * @return array Chris@18: * The metadata. Chris@18: */ Chris@18: public function getMeta() { Chris@18: return $this->meta; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Determines if two ResourceIdentifiers are the same. Chris@18: * Chris@18: * This method does not consider parallel relationships with different arity Chris@18: * values to be duplicates. For that, use the isParallel() method. Chris@18: * Chris@18: * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $a Chris@18: * The first ResourceIdentifier object. Chris@18: * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $b Chris@18: * The second ResourceIdentifier object. Chris@18: * Chris@18: * @return bool Chris@18: * TRUE if both relationships reference the same resource and do not have Chris@18: * two distinct arity's, FALSE otherwise. Chris@18: * Chris@18: * For example, if $a and $b both reference the same resource identifier, Chris@18: * they can only be distinct if they *both* have an arity and those values Chris@18: * are not the same. If $a or $b does not have an arity, they will be Chris@18: * considered duplicates. Chris@18: */ Chris@18: public static function isDuplicate(ResourceIdentifier $a, ResourceIdentifier $b) { Chris@18: return static::compare($a, $b) === 0; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Determines if two ResourceIdentifiers identify the same resource object. Chris@18: * Chris@18: * This method does not consider arity. Chris@18: * Chris@18: * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $a Chris@18: * The first ResourceIdentifier object. Chris@18: * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $b Chris@18: * The second ResourceIdentifier object. Chris@18: * Chris@18: * @return bool Chris@18: * TRUE if both relationships reference the same resource, even when they Chris@18: * have differing arity values, FALSE otherwise. Chris@18: */ Chris@18: public static function isParallel(ResourceIdentifier $a, ResourceIdentifier $b) { Chris@18: return static::compare($a->withArity(0), $b->withArity(0)) === 0; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Compares ResourceIdentifier objects. Chris@18: * Chris@18: * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $a Chris@18: * The first ResourceIdentifier object. Chris@18: * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier $b Chris@18: * The second ResourceIdentifier object. Chris@18: * Chris@18: * @return int Chris@18: * Returns 0 if $a and $b are duplicate ResourceIdentifiers. If $a and $b Chris@18: * identify the same resource but have distinct arity values, then the Chris@18: * return value will be arity $a minus arity $b. -1 otherwise. Chris@18: */ Chris@18: public static function compare(ResourceIdentifier $a, ResourceIdentifier $b) { Chris@18: $result = strcmp(sprintf('%s:%s', $a->getTypeName(), $a->getId()), sprintf('%s:%s', $b->getTypeName(), $b->getId())); Chris@18: // If type and ID do not match, return their ordering. Chris@18: if ($result !== 0) { Chris@18: return $result; Chris@18: } Chris@18: // If both $a and $b have an arity, then return the order by arity. Chris@18: // Otherwise, they are considered equal. Chris@18: return $a->hasArity() && $b->hasArity() Chris@18: ? $a->getArity() - $b->getArity() Chris@18: : 0; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Deduplicates an array of ResourceIdentifier objects. Chris@18: * Chris@18: * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier[] $resource_identifiers Chris@18: * The list of ResourceIdentifiers to deduplicate. Chris@18: * Chris@18: * @return \Drupal\jsonapi\JsonApiResource\ResourceIdentifier[] Chris@18: * A deduplicated array of ResourceIdentifier objects. Chris@18: * Chris@18: * @see self::isDuplicate() Chris@18: */ Chris@18: public static function deduplicate(array $resource_identifiers) { Chris@18: return array_reduce(array_slice($resource_identifiers, 1), function ($deduplicated, $current) { Chris@18: assert($current instanceof static); Chris@18: return array_merge($deduplicated, array_reduce($deduplicated, function ($duplicate, $previous) use ($current) { Chris@18: return $duplicate ?: static::isDuplicate($previous, $current); Chris@18: }, FALSE) ? [] : [$current]); Chris@18: }, array_slice($resource_identifiers, 0, 1)); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Determines if an array of ResourceIdentifier objects is duplicate free. Chris@18: * Chris@18: * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifier[] $resource_identifiers Chris@18: * The list of ResourceIdentifiers to assess. Chris@18: * Chris@18: * @return bool Chris@18: * Whether all the given resource identifiers are unique. Chris@18: */ Chris@18: public static function areResourceIdentifiersUnique(array $resource_identifiers) { Chris@18: return count($resource_identifiers) === count(static::deduplicate($resource_identifiers)); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Creates a ResourceIdentifier object. Chris@18: * Chris@18: * @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item Chris@18: * The entity reference field item from which to create the relationship. Chris@18: * @param int $arity Chris@18: * (optional) The arity of the relationship. Chris@18: * Chris@18: * @return self Chris@18: * A new ResourceIdentifier object. Chris@18: */ Chris@18: public static function toResourceIdentifier(EntityReferenceItem $item, $arity = NULL) { Chris@18: $property_name = static::getDataReferencePropertyName($item); Chris@18: $target = $item->get($property_name)->getValue(); Chris@18: if ($target === NULL) { Chris@18: return static::getVirtualOrMissingResourceIdentifier($item); Chris@18: } Chris@18: assert($target instanceof EntityInterface); Chris@18: /* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository */ Chris@18: $resource_type_repository = \Drupal::service('jsonapi.resource_type.repository'); Chris@18: $resource_type = $resource_type_repository->get($target->getEntityTypeId(), $target->bundle()); Chris@18: // Remove unwanted properties from the meta value, usually 'entity' Chris@18: // and 'target_id'. Chris@18: $properties = TypedDataInternalPropertiesHelper::getNonInternalProperties($item); Chris@18: $meta = array_diff_key($properties, array_flip([$property_name, $item->getDataDefinition()->getMainPropertyName()])); Chris@18: if (!is_null($arity)) { Chris@18: $meta[static::ARITY_KEY] = $arity; Chris@18: } Chris@18: return new static($resource_type, $target->uuid(), $meta); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Creates an array of ResourceIdentifier objects. Chris@18: * Chris@18: * @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items Chris@18: * The entity reference field items from which to create the relationship Chris@18: * array. Chris@18: * Chris@18: * @return self[] Chris@18: * An array of new ResourceIdentifier objects with appropriate arity values. Chris@18: */ Chris@18: public static function toResourceIdentifiers(EntityReferenceFieldItemListInterface $items) { Chris@18: $relationships = []; Chris@18: foreach ($items as $item) { Chris@18: // Create a ResourceIdentifier from the field item. This will make it Chris@18: // comparable with all previous field items. Here, it is assumed that the Chris@18: // resource identifier is unique so it has no arity. If a parallel Chris@18: // relationship is encountered, it will be assigned later. Chris@18: $relationship = static::toResourceIdentifier($item); Chris@18: // Now, iterate over the previously seen resource identifiers in reverse Chris@18: // order. Reverse order is important so that when a parallel relationship Chris@18: // is encountered, it will have the highest arity value so the current Chris@18: // relationship's arity value can simply be incremented by one. Chris@18: /* @var self $existing */ Chris@18: foreach (array_reverse($relationships, TRUE) as $index => $existing) { Chris@18: $is_parallel = static::isParallel($existing, $relationship); Chris@18: if ($is_parallel) { Chris@18: // A parallel relationship has been found. If the previous Chris@18: // relationship does not have an arity, it must now be assigned an Chris@18: // arity of 0. Chris@18: if (!$existing->hasArity()) { Chris@18: $relationships[$index] = $existing->withArity(0); Chris@18: } Chris@18: // Since the new ResourceIdentifier is parallel, it must have an arity Chris@18: // assigned to it that is the arity of the last parallel Chris@18: // relationship's arity + 1. Chris@18: $relationship = $relationship->withArity($relationships[$index]->getArity() + 1); Chris@18: break; Chris@18: } Chris@18: } Chris@18: // Finally, append the relationship to the list of ResourceIdentifiers. Chris@18: $relationships[] = $relationship; Chris@18: } Chris@18: return $relationships; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Creates an array of ResourceIdentifier objects with arity on every value. Chris@18: * Chris@18: * @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items Chris@18: * The entity reference field items from which to create the relationship Chris@18: * array. Chris@18: * Chris@18: * @return self[] Chris@18: * An array of new ResourceIdentifier objects with appropriate arity values. Chris@18: * Unlike self::toResourceIdentifiers(), this method does not omit arity Chris@18: * when an identifier is not parallel to any other identifier. Chris@18: */ Chris@18: public static function toResourceIdentifiersWithArityRequired(EntityReferenceFieldItemListInterface $items) { Chris@18: return array_map(function (ResourceIdentifier $identifier) { Chris@18: return $identifier->hasArity() ? $identifier : $identifier->withArity(0); Chris@18: }, static::toResourceIdentifiers($items)); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Creates a ResourceIdentifier object. Chris@18: * Chris@18: * @param \Drupal\Core\Entity\EntityInterface $entity Chris@18: * The entity from which to create the resource identifier. Chris@18: * Chris@18: * @return self Chris@18: * A new ResourceIdentifier object. Chris@18: */ Chris@18: public static function fromEntity(EntityInterface $entity) { Chris@18: /* @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository */ Chris@18: $resource_type_repository = \Drupal::service('jsonapi.resource_type.repository'); Chris@18: $resource_type = $resource_type_repository->get($entity->getEntityTypeId(), $entity->bundle()); Chris@18: return new static($resource_type, $entity->uuid()); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Helper method to determine which field item property contains an entity. Chris@18: * Chris@18: * @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item Chris@18: * The entity reference item for which to determine the entity property Chris@18: * name. Chris@18: * Chris@18: * @return string Chris@18: * The property name which has an entity as its value. Chris@18: */ Chris@18: protected static function getDataReferencePropertyName(EntityReferenceItem $item) { Chris@18: foreach ($item->getDataDefinition()->getPropertyDefinitions() as $property_name => $property_definition) { Chris@18: if ($property_definition instanceof DataReferenceDefinitionInterface) { Chris@18: return $property_name; Chris@18: } Chris@18: } Chris@18: } Chris@18: Chris@18: /** Chris@18: * Creates a ResourceIdentifier for a NULL or FALSE entity reference item. Chris@18: * Chris@18: * @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item Chris@18: * The entity reference field item. Chris@18: * Chris@18: * @return self Chris@18: * A new ResourceIdentifier object. Chris@18: */ Chris@18: protected static function getVirtualOrMissingResourceIdentifier(EntityReferenceItem $item) { Chris@18: $resource_type_repository = \Drupal::service('jsonapi.resource_type.repository'); Chris@18: $property_name = static::getDataReferencePropertyName($item); Chris@18: $value = $item->get($property_name)->getValue(); Chris@18: assert($value === NULL); Chris@18: $field = $item->getParent(); Chris@18: assert($field instanceof EntityReferenceFieldItemListInterface); Chris@18: $host_entity = $field->getEntity(); Chris@18: assert($host_entity instanceof EntityInterface); Chris@18: $resource_type = $resource_type_repository->get($host_entity->getEntityTypeId(), $host_entity->bundle()); Chris@18: assert($resource_type instanceof ResourceType); Chris@18: $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($field->getName()); Chris@18: $get_metadata = function ($type) { Chris@18: return [ Chris@18: 'links' => [ Chris@18: 'help' => [ Chris@18: 'href' => "https://www.drupal.org/docs/8/modules/json-api/core-concepts#$type", Chris@18: 'meta' => [ Chris@18: 'about' => "Usage and meaning of the '$type' resource identifier.", Chris@18: ], Chris@18: ], Chris@18: ], Chris@18: ]; Chris@18: }; Chris@18: $resource_type = reset($relatable_resource_types); Chris@18: // A non-empty entity reference field that refers to a non-existent entity Chris@18: // is not a data integrity problem. For example, Term entities' "parent" Chris@18: // entity reference field uses target_id zero to refer to the non-existent Chris@18: // "" term. And references to entities that no longer exist are not Chris@18: // cleaned up by Drupal; hence we map it to a "missing" resource. Chris@18: if ($field->getFieldDefinition()->getSetting('target_type') === 'taxonomy_term' && $item->get('target_id')->getCastedValue() === 0) { Chris@18: if (count($relatable_resource_types) !== 1) { Chris@18: throw new \RuntimeException('Relationships to virtual resources are possible only if a single resource type is relatable.'); Chris@18: } Chris@18: return new static($resource_type, 'virtual', $get_metadata('virtual')); Chris@18: } Chris@18: else { Chris@18: // In case of a dangling reference, it is impossible to determine which Chris@18: // resource type it used to reference, because that requires knowing the Chris@18: // referenced bundle, which Drupal does not store. Chris@18: // If we can reliably determine the resource type of the dangling Chris@18: // reference, use it; otherwise conjure a fake resource type out of thin Chris@18: // air, one that indicates we don't know the bundle. Chris@18: $resource_type = count($relatable_resource_types) > 1 Chris@18: ? new ResourceType('?', '?', '') Chris@18: : reset($relatable_resource_types); Chris@18: return new static($resource_type, 'missing', $get_metadata('missing')); Chris@18: } Chris@18: } Chris@18: Chris@18: }