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 }
|