diff core/modules/jsonapi/src/JsonApiResource/ResourceObject.php @ 18:af1871eacc83

Update to Drupal core 8.7.1
author Chris Cannam
date Thu, 09 May 2019 15:33:08 +0100
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/modules/jsonapi/src/JsonApiResource/ResourceObject.php	Thu May 09 15:33:08 2019 +0100
@@ -0,0 +1,349 @@
+<?php
+
+namespace Drupal\jsonapi\JsonApiResource;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyTrait;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
+use Drupal\Core\Url;
+use Drupal\jsonapi\JsonApiSpec;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Drupal\jsonapi\Revisions\VersionByRel;
+use Drupal\jsonapi\Routing\Routes;
+
+/**
+ * Represents a JSON:API resource object.
+ *
+ * This value object wraps a Drupal entity so that it can carry a JSON:API
+ * resource type object alongside it. It also helps abstract away differences
+ * between config and content entities within the JSON:API codebase.
+ *
+ * @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
+ */
+class ResourceObject implements CacheableDependencyInterface, ResourceIdentifierInterface {
+
+  use CacheableDependencyTrait;
+  use ResourceIdentifierTrait;
+
+  /**
+   * The resource object's version identifier.
+   *
+   * @var string|null
+   */
+  protected $versionIdentifier;
+
+  /**
+   * The object's fields.
+   *
+   * This refers to "fields" in the JSON:API sense of the word. Config entities
+   * do not have real fields, so in that case, this will be an array of values
+   * for config entity attributes.
+   *
+   * @var \Drupal\Core\Field\FieldItemListInterface[]|mixed[]
+   */
+  protected $fields;
+
+  /**
+   * The resource object's links.
+   *
+   * @var \Drupal\jsonapi\JsonApiResource\LinkCollection
+   */
+  protected $links;
+
+  /**
+   * ResourceObject constructor.
+   *
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface $cacheability
+   *   The cacheability for the resource object.
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The JSON:API resource type of the resource object.
+   * @param string $id
+   *   The resource object's ID.
+   * @param mixed|null $revision_id
+   *   The resource object's version identifier. NULL, if the resource object is
+   *   not versionable.
+   * @param array $fields
+   *   An array of the resource object's fields, keyed by public field name.
+   * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $links
+   *   The links for the resource object.
+   */
+  public function __construct(CacheableDependencyInterface $cacheability, ResourceType $resource_type, $id, $revision_id, array $fields, LinkCollection $links) {
+    assert(is_null($revision_id) || $resource_type->isVersionable());
+    $this->setCacheability($cacheability);
+    $this->resourceType = $resource_type;
+    $this->resourceIdentifier = new ResourceIdentifier($resource_type, $id);
+    $this->versionIdentifier = $revision_id ? 'id:' . $revision_id : NULL;
+    $this->fields = $fields;
+    $this->links = $links->withContext($this);
+  }
+
+  /**
+   * Creates a new ResourceObject from an entity.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The JSON:API resource type of the resource object.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to be represented by this resource object.
+   * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $links
+   *   (optional) Any links for the resource object, if a `self` link is not
+   *   provided, one will be automatically added if the resource is locatable
+   *   and is not an internal entity.
+   *
+   * @return static
+   *   An instantiated resource object.
+   */
+  public static function createFromEntity(ResourceType $resource_type, EntityInterface $entity, LinkCollection $links = NULL) {
+    return new static(
+      $entity,
+      $resource_type,
+      $entity->uuid(),
+      $resource_type->isVersionable() && $entity instanceof RevisionableInterface ? $entity->getRevisionId() : NULL,
+      static::extractFieldsFromEntity($resource_type, $entity),
+      static::buildLinksFromEntity($resource_type, $entity, $links ?: new LinkCollection([]))
+    );
+  }
+
+  /**
+   * Whether the resource object has the given field.
+   *
+   * @param string $public_field_name
+   *   A public field name.
+   *
+   * @return bool
+   *   TRUE if the resource object has the given field, FALSE otherwise.
+   */
+  public function hasField($public_field_name) {
+    return isset($this->fields[$public_field_name]);
+  }
+
+  /**
+   * Gets the given field.
+   *
+   * @param string $public_field_name
+   *   A public field name.
+   *
+   * @return mixed|\Drupal\Core\Field\FieldItemListInterface|null
+   *   The field or NULL if the resource object does not have the given field.
+   *
+   * @see ::extractFields()
+   */
+  public function getField($public_field_name) {
+    return $this->hasField($public_field_name) ? $this->fields[$public_field_name] : NULL;
+  }
+
+  /**
+   * Gets the ResourceObject's fields.
+   *
+   * @return array
+   *   The resource object's fields, keyed by public field name.
+   *
+   * @see ::extractFields()
+   */
+  public function getFields() {
+    return $this->fields;
+  }
+
+  /**
+   * Gets the ResourceObject's links.
+   *
+   * @return \Drupal\jsonapi\JsonApiResource\LinkCollection
+   *   The resource object's links.
+   */
+  public function getLinks() {
+    return $this->links;
+  }
+
+  /**
+   * Gets a version identifier for the ResourceObject.
+   *
+   * @return string
+   *   The version identifier of the resource object, if the resource type is
+   *   versionable.
+   */
+  public function getVersionIdentifier() {
+    if (!$this->resourceType->isVersionable()) {
+      throw new \LogicException('Cannot get a version identifier for a non-versionable resource.');
+    }
+    return $this->versionIdentifier;
+  }
+
+  /**
+   * Gets a Url for the ResourceObject.
+   *
+   * @return \Drupal\Core\Url
+   *   The URL for the identified resource object.
+   *
+   * @throws \LogicException
+   *   Thrown if the resource object is not locatable.
+   *
+   * @see \Drupal\jsonapi\ResourceType\ResourceTypeRepository::isLocatableResourceType()
+   */
+  public function toUrl() {
+    foreach ($this->links as $key => $link) {
+      if ($key === 'self') {
+        $first = reset($link);
+        return $first->getUri();
+      }
+    }
+    throw new \LogicException('A Url does not exist for this resource object because its resource type is not locatable.');
+  }
+
+  /**
+   * Extracts the entity's fields.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The JSON:API resource type of the given entity.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity from which fields should be extracted.
+   *
+   * @return mixed|\Drupal\Core\Field\FieldItemListInterface[]
+   *   If the resource object represents a content entity, the fields will be
+   *   objects satisfying FieldItemListInterface. If it represents a config
+   *   entity, the fields will be scalar values or arrays.
+   */
+  protected static function extractFieldsFromEntity(ResourceType $resource_type, EntityInterface $entity) {
+    assert($entity instanceof ContentEntityInterface || $entity instanceof ConfigEntityInterface);
+    return $entity instanceof ContentEntityInterface
+      ? static::extractContentEntityFields($resource_type, $entity)
+      : static::extractConfigEntityFields($resource_type, $entity);
+  }
+
+  /**
+   * Builds a LinkCollection for the given entity.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The JSON:API resource type of the given entity.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity for which to build links.
+   * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $links
+   *   (optional) Any extra links for the resource object, if a `self` link is
+   *   not provided, one will be automatically added if the resource is
+   *   locatable and is not an internal entity.
+   *
+   * @return \Drupal\jsonapi\JsonApiResource\LinkCollection
+   *   The built links.
+   */
+  protected static function buildLinksFromEntity(ResourceType $resource_type, EntityInterface $entity, LinkCollection $links) {
+    if ($resource_type->isLocatable() && !$resource_type->isInternal()) {
+      $self_url = Url::fromRoute(Routes::getRouteName($resource_type, 'individual'), ['entity' => $entity->uuid()]);
+      if ($resource_type->isVersionable()) {
+        assert($entity instanceof RevisionableInterface);
+        if (!$links->hasLinkWithKey('self')) {
+          // If the resource is versionable, the `self` link should be the exact
+          // link for the represented version. This helps a client track
+          // revision changes and to disambiguate resource objects with the same
+          // `type` and `id` in a `version-history` collection.
+          $self_with_version_url = $self_url->setOption('query', [JsonApiSpec::VERSION_QUERY_PARAMETER => 'id:' . $entity->getRevisionId()]);
+          $links = $links->withLink('self', new Link(new CacheableMetadata(), $self_with_version_url, ['self']));
+        }
+        if (!$entity->isDefaultRevision()) {
+          $latest_version_url = $self_url->setOption('query', [JsonApiSpec::VERSION_QUERY_PARAMETER => 'rel:' . VersionByRel::LATEST_VERSION]);
+          $links = $links->withLink(VersionByRel::LATEST_VERSION, new Link(new CacheableMetadata(), $latest_version_url, [VersionByRel::LATEST_VERSION]));
+        }
+        if (!$entity->isLatestRevision()) {
+          $working_copy_url = $self_url->setOption('query', [JsonApiSpec::VERSION_QUERY_PARAMETER => 'rel:' . VersionByRel::WORKING_COPY]);
+          $links = $links->withLink(VersionByRel::WORKING_COPY, new Link(new CacheableMetadata(), $working_copy_url, [VersionByRel::WORKING_COPY]));
+        }
+      }
+      if (!$links->hasLinkWithKey('self')) {
+        $links = $links->withLink('self', new Link(new CacheableMetadata(), $self_url, ['self']));
+      }
+    }
+    return $links;
+  }
+
+  /**
+   * Extracts a content entity's fields.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The JSON:API resource type of the given entity.
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   The config entity from which fields should be extracted.
+   *
+   * @return \Drupal\Core\Field\FieldItemListInterface[]
+   *   The fields extracted from a content entity.
+   */
+  protected static function extractContentEntityFields(ResourceType $resource_type, ContentEntityInterface $entity) {
+    $output = [];
+    $fields = TypedDataInternalPropertiesHelper::getNonInternalProperties($entity->getTypedData());
+    // Filter the array based on the field names.
+    $enabled_field_names = array_filter(
+      array_keys($fields),
+      [$resource_type, 'isFieldEnabled']
+    );
+
+    // The "label" field needs special treatment: some entity types have a label
+    // field that is actually backed by a label callback.
+    $entity_type = $entity->getEntityType();
+    if ($entity_type->hasLabelCallback()) {
+      $fields[static::getLabelFieldName($entity)]->value = $entity->label();
+    }
+
+    // Return a sub-array of $output containing the keys in $enabled_fields.
+    $input = array_intersect_key($fields, array_flip($enabled_field_names));
+    foreach ($input as $field_name => $field_value) {
+      $public_field_name = $resource_type->getPublicName($field_name);
+      $output[$public_field_name] = $field_value;
+    }
+    return $output;
+  }
+
+  /**
+   * Determines the entity type's (internal) label field name.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity from which fields should be extracted.
+   *
+   * @return string
+   *   The label field name.
+   */
+  protected static function getLabelFieldName(EntityInterface $entity) {
+    $label_field_name = $entity->getEntityType()->getKey('label');
+    // @todo Remove this work-around after https://www.drupal.org/project/drupal/issues/2450793 lands.
+    if ($entity->getEntityTypeId() === 'user') {
+      $label_field_name = 'name';
+    }
+    return $label_field_name;
+  }
+
+  /**
+   * Extracts a config entity's fields.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The JSON:API resource type of the given entity.
+   * @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity
+   *   The config entity from which fields should be extracted.
+   *
+   * @return array
+   *   The fields extracted from a config entity.
+   */
+  protected static function extractConfigEntityFields(ResourceType $resource_type, ConfigEntityInterface $entity) {
+    $enabled_public_fields = [];
+    $fields = $entity->toArray();
+    // Filter the array based on the field names.
+    $enabled_field_names = array_filter(array_keys($fields), function ($internal_field_name) use ($resource_type) {
+      // Config entities have "fields" which aren't known to the resource type,
+      // these fields should not be excluded because they cannot be enabled or
+      // disabled.
+      return !$resource_type->hasField($internal_field_name) || $resource_type->isFieldEnabled($internal_field_name);
+    });
+    // Return a sub-array of $output containing the keys in $enabled_fields.
+    $input = array_intersect_key($fields, array_flip($enabled_field_names));
+    /* @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
+    foreach ($input as $field_name => $field_value) {
+      $public_field_name = $resource_type->getPublicName($field_name);
+      $enabled_public_fields[$public_field_name] = $field_value;
+    }
+    return $enabled_public_fields;
+  }
+
+}