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