annotate core/modules/jsonapi/src/JsonApiResource/ResourceObject.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@18 1 <?php
Chris@18 2
Chris@18 3 namespace Drupal\jsonapi\JsonApiResource;
Chris@18 4
Chris@18 5 use Drupal\Core\Cache\CacheableDependencyInterface;
Chris@18 6 use Drupal\Core\Cache\CacheableDependencyTrait;
Chris@18 7 use Drupal\Core\Cache\CacheableMetadata;
Chris@18 8 use Drupal\Core\Config\Entity\ConfigEntityInterface;
Chris@18 9 use Drupal\Core\Entity\ContentEntityInterface;
Chris@18 10 use Drupal\Core\Entity\EntityInterface;
Chris@18 11 use Drupal\Core\Entity\RevisionableInterface;
Chris@18 12 use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
Chris@18 13 use Drupal\Core\Url;
Chris@18 14 use Drupal\jsonapi\JsonApiSpec;
Chris@18 15 use Drupal\jsonapi\ResourceType\ResourceType;
Chris@18 16 use Drupal\jsonapi\Revisions\VersionByRel;
Chris@18 17 use Drupal\jsonapi\Routing\Routes;
Chris@18 18
Chris@18 19 /**
Chris@18 20 * Represents a JSON:API resource object.
Chris@18 21 *
Chris@18 22 * This value object wraps a Drupal entity so that it can carry a JSON:API
Chris@18 23 * resource type object alongside it. It also helps abstract away differences
Chris@18 24 * between config and content entities within the JSON:API codebase.
Chris@18 25 *
Chris@18 26 * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
Chris@18 27 * may change at any time and could break any dependencies on it.
Chris@18 28 *
Chris@18 29 * @see https://www.drupal.org/project/jsonapi/issues/3032787
Chris@18 30 * @see jsonapi.api.php
Chris@18 31 */
Chris@18 32 class ResourceObject implements CacheableDependencyInterface, ResourceIdentifierInterface {
Chris@18 33
Chris@18 34 use CacheableDependencyTrait;
Chris@18 35 use ResourceIdentifierTrait;
Chris@18 36
Chris@18 37 /**
Chris@18 38 * The resource object's version identifier.
Chris@18 39 *
Chris@18 40 * @var string|null
Chris@18 41 */
Chris@18 42 protected $versionIdentifier;
Chris@18 43
Chris@18 44 /**
Chris@18 45 * The object's fields.
Chris@18 46 *
Chris@18 47 * This refers to "fields" in the JSON:API sense of the word. Config entities
Chris@18 48 * do not have real fields, so in that case, this will be an array of values
Chris@18 49 * for config entity attributes.
Chris@18 50 *
Chris@18 51 * @var \Drupal\Core\Field\FieldItemListInterface[]|mixed[]
Chris@18 52 */
Chris@18 53 protected $fields;
Chris@18 54
Chris@18 55 /**
Chris@18 56 * The resource object's links.
Chris@18 57 *
Chris@18 58 * @var \Drupal\jsonapi\JsonApiResource\LinkCollection
Chris@18 59 */
Chris@18 60 protected $links;
Chris@18 61
Chris@18 62 /**
Chris@18 63 * ResourceObject constructor.
Chris@18 64 *
Chris@18 65 * @param \Drupal\Core\Cache\CacheableDependencyInterface $cacheability
Chris@18 66 * The cacheability for the resource object.
Chris@18 67 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
Chris@18 68 * The JSON:API resource type of the resource object.
Chris@18 69 * @param string $id
Chris@18 70 * The resource object's ID.
Chris@18 71 * @param mixed|null $revision_id
Chris@18 72 * The resource object's version identifier. NULL, if the resource object is
Chris@18 73 * not versionable.
Chris@18 74 * @param array $fields
Chris@18 75 * An array of the resource object's fields, keyed by public field name.
Chris@18 76 * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $links
Chris@18 77 * The links for the resource object.
Chris@18 78 */
Chris@18 79 public function __construct(CacheableDependencyInterface $cacheability, ResourceType $resource_type, $id, $revision_id, array $fields, LinkCollection $links) {
Chris@18 80 assert(is_null($revision_id) || $resource_type->isVersionable());
Chris@18 81 $this->setCacheability($cacheability);
Chris@18 82 $this->resourceType = $resource_type;
Chris@18 83 $this->resourceIdentifier = new ResourceIdentifier($resource_type, $id);
Chris@18 84 $this->versionIdentifier = $revision_id ? 'id:' . $revision_id : NULL;
Chris@18 85 $this->fields = $fields;
Chris@18 86 $this->links = $links->withContext($this);
Chris@18 87 }
Chris@18 88
Chris@18 89 /**
Chris@18 90 * Creates a new ResourceObject from an entity.
Chris@18 91 *
Chris@18 92 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
Chris@18 93 * The JSON:API resource type of the resource object.
Chris@18 94 * @param \Drupal\Core\Entity\EntityInterface $entity
Chris@18 95 * The entity to be represented by this resource object.
Chris@18 96 * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $links
Chris@18 97 * (optional) Any links for the resource object, if a `self` link is not
Chris@18 98 * provided, one will be automatically added if the resource is locatable
Chris@18 99 * and is not an internal entity.
Chris@18 100 *
Chris@18 101 * @return static
Chris@18 102 * An instantiated resource object.
Chris@18 103 */
Chris@18 104 public static function createFromEntity(ResourceType $resource_type, EntityInterface $entity, LinkCollection $links = NULL) {
Chris@18 105 return new static(
Chris@18 106 $entity,
Chris@18 107 $resource_type,
Chris@18 108 $entity->uuid(),
Chris@18 109 $resource_type->isVersionable() && $entity instanceof RevisionableInterface ? $entity->getRevisionId() : NULL,
Chris@18 110 static::extractFieldsFromEntity($resource_type, $entity),
Chris@18 111 static::buildLinksFromEntity($resource_type, $entity, $links ?: new LinkCollection([]))
Chris@18 112 );
Chris@18 113 }
Chris@18 114
Chris@18 115 /**
Chris@18 116 * Whether the resource object has the given field.
Chris@18 117 *
Chris@18 118 * @param string $public_field_name
Chris@18 119 * A public field name.
Chris@18 120 *
Chris@18 121 * @return bool
Chris@18 122 * TRUE if the resource object has the given field, FALSE otherwise.
Chris@18 123 */
Chris@18 124 public function hasField($public_field_name) {
Chris@18 125 return isset($this->fields[$public_field_name]);
Chris@18 126 }
Chris@18 127
Chris@18 128 /**
Chris@18 129 * Gets the given field.
Chris@18 130 *
Chris@18 131 * @param string $public_field_name
Chris@18 132 * A public field name.
Chris@18 133 *
Chris@18 134 * @return mixed|\Drupal\Core\Field\FieldItemListInterface|null
Chris@18 135 * The field or NULL if the resource object does not have the given field.
Chris@18 136 *
Chris@18 137 * @see ::extractFields()
Chris@18 138 */
Chris@18 139 public function getField($public_field_name) {
Chris@18 140 return $this->hasField($public_field_name) ? $this->fields[$public_field_name] : NULL;
Chris@18 141 }
Chris@18 142
Chris@18 143 /**
Chris@18 144 * Gets the ResourceObject's fields.
Chris@18 145 *
Chris@18 146 * @return array
Chris@18 147 * The resource object's fields, keyed by public field name.
Chris@18 148 *
Chris@18 149 * @see ::extractFields()
Chris@18 150 */
Chris@18 151 public function getFields() {
Chris@18 152 return $this->fields;
Chris@18 153 }
Chris@18 154
Chris@18 155 /**
Chris@18 156 * Gets the ResourceObject's links.
Chris@18 157 *
Chris@18 158 * @return \Drupal\jsonapi\JsonApiResource\LinkCollection
Chris@18 159 * The resource object's links.
Chris@18 160 */
Chris@18 161 public function getLinks() {
Chris@18 162 return $this->links;
Chris@18 163 }
Chris@18 164
Chris@18 165 /**
Chris@18 166 * Gets a version identifier for the ResourceObject.
Chris@18 167 *
Chris@18 168 * @return string
Chris@18 169 * The version identifier of the resource object, if the resource type is
Chris@18 170 * versionable.
Chris@18 171 */
Chris@18 172 public function getVersionIdentifier() {
Chris@18 173 if (!$this->resourceType->isVersionable()) {
Chris@18 174 throw new \LogicException('Cannot get a version identifier for a non-versionable resource.');
Chris@18 175 }
Chris@18 176 return $this->versionIdentifier;
Chris@18 177 }
Chris@18 178
Chris@18 179 /**
Chris@18 180 * Gets a Url for the ResourceObject.
Chris@18 181 *
Chris@18 182 * @return \Drupal\Core\Url
Chris@18 183 * The URL for the identified resource object.
Chris@18 184 *
Chris@18 185 * @throws \LogicException
Chris@18 186 * Thrown if the resource object is not locatable.
Chris@18 187 *
Chris@18 188 * @see \Drupal\jsonapi\ResourceType\ResourceTypeRepository::isLocatableResourceType()
Chris@18 189 */
Chris@18 190 public function toUrl() {
Chris@18 191 foreach ($this->links as $key => $link) {
Chris@18 192 if ($key === 'self') {
Chris@18 193 $first = reset($link);
Chris@18 194 return $first->getUri();
Chris@18 195 }
Chris@18 196 }
Chris@18 197 throw new \LogicException('A Url does not exist for this resource object because its resource type is not locatable.');
Chris@18 198 }
Chris@18 199
Chris@18 200 /**
Chris@18 201 * Extracts the entity's fields.
Chris@18 202 *
Chris@18 203 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
Chris@18 204 * The JSON:API resource type of the given entity.
Chris@18 205 * @param \Drupal\Core\Entity\EntityInterface $entity
Chris@18 206 * The entity from which fields should be extracted.
Chris@18 207 *
Chris@18 208 * @return mixed|\Drupal\Core\Field\FieldItemListInterface[]
Chris@18 209 * If the resource object represents a content entity, the fields will be
Chris@18 210 * objects satisfying FieldItemListInterface. If it represents a config
Chris@18 211 * entity, the fields will be scalar values or arrays.
Chris@18 212 */
Chris@18 213 protected static function extractFieldsFromEntity(ResourceType $resource_type, EntityInterface $entity) {
Chris@18 214 assert($entity instanceof ContentEntityInterface || $entity instanceof ConfigEntityInterface);
Chris@18 215 return $entity instanceof ContentEntityInterface
Chris@18 216 ? static::extractContentEntityFields($resource_type, $entity)
Chris@18 217 : static::extractConfigEntityFields($resource_type, $entity);
Chris@18 218 }
Chris@18 219
Chris@18 220 /**
Chris@18 221 * Builds a LinkCollection for the given entity.
Chris@18 222 *
Chris@18 223 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
Chris@18 224 * The JSON:API resource type of the given entity.
Chris@18 225 * @param \Drupal\Core\Entity\EntityInterface $entity
Chris@18 226 * The entity for which to build links.
Chris@18 227 * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $links
Chris@18 228 * (optional) Any extra links for the resource object, if a `self` link is
Chris@18 229 * not provided, one will be automatically added if the resource is
Chris@18 230 * locatable and is not an internal entity.
Chris@18 231 *
Chris@18 232 * @return \Drupal\jsonapi\JsonApiResource\LinkCollection
Chris@18 233 * The built links.
Chris@18 234 */
Chris@18 235 protected static function buildLinksFromEntity(ResourceType $resource_type, EntityInterface $entity, LinkCollection $links) {
Chris@18 236 if ($resource_type->isLocatable() && !$resource_type->isInternal()) {
Chris@18 237 $self_url = Url::fromRoute(Routes::getRouteName($resource_type, 'individual'), ['entity' => $entity->uuid()]);
Chris@18 238 if ($resource_type->isVersionable()) {
Chris@18 239 assert($entity instanceof RevisionableInterface);
Chris@18 240 if (!$links->hasLinkWithKey('self')) {
Chris@18 241 // If the resource is versionable, the `self` link should be the exact
Chris@18 242 // link for the represented version. This helps a client track
Chris@18 243 // revision changes and to disambiguate resource objects with the same
Chris@18 244 // `type` and `id` in a `version-history` collection.
Chris@18 245 $self_with_version_url = $self_url->setOption('query', [JsonApiSpec::VERSION_QUERY_PARAMETER => 'id:' . $entity->getRevisionId()]);
Chris@18 246 $links = $links->withLink('self', new Link(new CacheableMetadata(), $self_with_version_url, ['self']));
Chris@18 247 }
Chris@18 248 if (!$entity->isDefaultRevision()) {
Chris@18 249 $latest_version_url = $self_url->setOption('query', [JsonApiSpec::VERSION_QUERY_PARAMETER => 'rel:' . VersionByRel::LATEST_VERSION]);
Chris@18 250 $links = $links->withLink(VersionByRel::LATEST_VERSION, new Link(new CacheableMetadata(), $latest_version_url, [VersionByRel::LATEST_VERSION]));
Chris@18 251 }
Chris@18 252 if (!$entity->isLatestRevision()) {
Chris@18 253 $working_copy_url = $self_url->setOption('query', [JsonApiSpec::VERSION_QUERY_PARAMETER => 'rel:' . VersionByRel::WORKING_COPY]);
Chris@18 254 $links = $links->withLink(VersionByRel::WORKING_COPY, new Link(new CacheableMetadata(), $working_copy_url, [VersionByRel::WORKING_COPY]));
Chris@18 255 }
Chris@18 256 }
Chris@18 257 if (!$links->hasLinkWithKey('self')) {
Chris@18 258 $links = $links->withLink('self', new Link(new CacheableMetadata(), $self_url, ['self']));
Chris@18 259 }
Chris@18 260 }
Chris@18 261 return $links;
Chris@18 262 }
Chris@18 263
Chris@18 264 /**
Chris@18 265 * Extracts a content entity's fields.
Chris@18 266 *
Chris@18 267 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
Chris@18 268 * The JSON:API resource type of the given entity.
Chris@18 269 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
Chris@18 270 * The config entity from which fields should be extracted.
Chris@18 271 *
Chris@18 272 * @return \Drupal\Core\Field\FieldItemListInterface[]
Chris@18 273 * The fields extracted from a content entity.
Chris@18 274 */
Chris@18 275 protected static function extractContentEntityFields(ResourceType $resource_type, ContentEntityInterface $entity) {
Chris@18 276 $output = [];
Chris@18 277 $fields = TypedDataInternalPropertiesHelper::getNonInternalProperties($entity->getTypedData());
Chris@18 278 // Filter the array based on the field names.
Chris@18 279 $enabled_field_names = array_filter(
Chris@18 280 array_keys($fields),
Chris@18 281 [$resource_type, 'isFieldEnabled']
Chris@18 282 );
Chris@18 283
Chris@18 284 // The "label" field needs special treatment: some entity types have a label
Chris@18 285 // field that is actually backed by a label callback.
Chris@18 286 $entity_type = $entity->getEntityType();
Chris@18 287 if ($entity_type->hasLabelCallback()) {
Chris@18 288 $fields[static::getLabelFieldName($entity)]->value = $entity->label();
Chris@18 289 }
Chris@18 290
Chris@18 291 // Return a sub-array of $output containing the keys in $enabled_fields.
Chris@18 292 $input = array_intersect_key($fields, array_flip($enabled_field_names));
Chris@18 293 foreach ($input as $field_name => $field_value) {
Chris@18 294 $public_field_name = $resource_type->getPublicName($field_name);
Chris@18 295 $output[$public_field_name] = $field_value;
Chris@18 296 }
Chris@18 297 return $output;
Chris@18 298 }
Chris@18 299
Chris@18 300 /**
Chris@18 301 * Determines the entity type's (internal) label field name.
Chris@18 302 *
Chris@18 303 * @param \Drupal\Core\Entity\EntityInterface $entity
Chris@18 304 * The entity from which fields should be extracted.
Chris@18 305 *
Chris@18 306 * @return string
Chris@18 307 * The label field name.
Chris@18 308 */
Chris@18 309 protected static function getLabelFieldName(EntityInterface $entity) {
Chris@18 310 $label_field_name = $entity->getEntityType()->getKey('label');
Chris@18 311 // @todo Remove this work-around after https://www.drupal.org/project/drupal/issues/2450793 lands.
Chris@18 312 if ($entity->getEntityTypeId() === 'user') {
Chris@18 313 $label_field_name = 'name';
Chris@18 314 }
Chris@18 315 return $label_field_name;
Chris@18 316 }
Chris@18 317
Chris@18 318 /**
Chris@18 319 * Extracts a config entity's fields.
Chris@18 320 *
Chris@18 321 * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
Chris@18 322 * The JSON:API resource type of the given entity.
Chris@18 323 * @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity
Chris@18 324 * The config entity from which fields should be extracted.
Chris@18 325 *
Chris@18 326 * @return array
Chris@18 327 * The fields extracted from a config entity.
Chris@18 328 */
Chris@18 329 protected static function extractConfigEntityFields(ResourceType $resource_type, ConfigEntityInterface $entity) {
Chris@18 330 $enabled_public_fields = [];
Chris@18 331 $fields = $entity->toArray();
Chris@18 332 // Filter the array based on the field names.
Chris@18 333 $enabled_field_names = array_filter(array_keys($fields), function ($internal_field_name) use ($resource_type) {
Chris@18 334 // Config entities have "fields" which aren't known to the resource type,
Chris@18 335 // these fields should not be excluded because they cannot be enabled or
Chris@18 336 // disabled.
Chris@18 337 return !$resource_type->hasField($internal_field_name) || $resource_type->isFieldEnabled($internal_field_name);
Chris@18 338 });
Chris@18 339 // Return a sub-array of $output containing the keys in $enabled_fields.
Chris@18 340 $input = array_intersect_key($fields, array_flip($enabled_field_names));
Chris@18 341 /* @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
Chris@18 342 foreach ($input as $field_name => $field_value) {
Chris@18 343 $public_field_name = $resource_type->getPublicName($field_name);
Chris@18 344 $enabled_public_fields[$public_field_name] = $field_value;
Chris@18 345 }
Chris@18 346 return $enabled_public_fields;
Chris@18 347 }
Chris@18 348
Chris@18 349 }